Valg af inheritance mapping: TPH, TPT eller TPC?

En af vores centrale use cases lyder sådan i brief format:

Underviseren opretter en Quest med relevante oplysninger og tilføjer QuestEnheder. Hver QuestEnhed har én Lokation og er enten en Aktivitet, Quiz eller Opgave. Questen kan oprettes som privat og senere deles eller offentliggøres.

Domænemodellen kan ses herunder:

Klik på billedet for at forstørre.

Det har givet anledning til nogle tanker, der har med databasedesign at gøre, og som jeg vil skrive om her.

I arbejdet med databasen til QuestGame er et af de designvalg, jeg er stødt på, hvordan arv i domænemodellen skal afspejles i den relationelle database. I vores projekt, hvor vi udvikler et mobilspil, har vi tre typer af udfordringer, man kan løse på en given Position:

  • Quiz (et spørgsmål og 2-4 svarmuligheder)
  • Aktivitet (en fysisk aktivitet, som skal udføres på stedet)
  • Opgave (en opgave, der skal løses – f.eks. tegne noget, tage et billede af noget bestemt)

I vores model har vi en fælles superklasse, QuestEnhed (QuestItem i systemet), som repræsenterer én af tre typer udfordringer: Activity, Task og Quiz.

Det er relevant, fordi en spiller, der joiner en Quest, får vist et interaktivt kort, hvor alle QuestItems er synlige fra start. Backend skal derfor kunne hente de nødvendige kortdata samlet: positionens id og koordinater, typen af det tilknyttede element samt spillerens status på positionen. Det betyder ikke, at alle detaljer om en Activity, Task eller Quiz skal indlæses med det samme, men systemet arbejder ofte med typerne gennem deres fælles begreb.

Her bliver EF Cores strategier for inheritance mapping relevante: Table Per Hierarchy (TPH), Table Per Type (TPT) og Table Per Concrete Type (TPC).

TPH – én tabel til hele hierarkiet

Med TPH gemmes hele arv-hierarkiet i én tabel. I QuestGame vil Activity, Task og Quiz derfor være rækker i samme QuestItems-tabel. Tabellen indeholder de fælles properties fra QuestItem og de properties, som kun tilhører bestemte undertyper. En discriminator-kolonne, som f.eks. kunne hedde ElementType, angiver, hvilken type den enkelte række repræsenterer.

En tabel kunne eksempelvis indeholde Id, Title, Points og en typeangivelse samt felter, der kun vedrører Quiz, Task eller Activity. Felter, som ikke er relevante for en bestemt række, vil være NULL.

Fordelen er især, at fælles forespørgsler bliver simple. Hvis backenden skal finde alle QuestItems til kortet, ligger elementerne i samme tabel. EF Core bruger desuden TPH som standardstrategi for arv. Microsoft fremhæver TPH som et godt generelt valg, særligt når applikationen ofte forespørger på basetypen eller flere undertyper samlet.

Ulempen er, at tabellen kan få mange nullable kolonner, hvis undertyperne udvikler sig i forskellige retninger. Det kan gøre skemaet mindre elegant, selv om tomme kolonner ikke nødvendigvis er et stort performanceproblem i sig selv.

TPT – en tabel pr. type

Med TPT får hver type i hierarkiet sin egen tabel. QuestItem får en tabel med de fælles egenskaber, mens Activity, Task og Quiz får separate tabeller med deres typespecifikke data. En Quiz vil derfor bestå af en række i QuestItems og en relateret række i Quizzes.

Fordelen er et mere normaliseret og overskueligt databaseskema. Der opstår ikke en lang række nullable kolonner, og hver tabel indeholder kun data, som giver mening for den pågældende type.

Til gengæld kræver det flere joins, når en konkret entitet skal rekonstrueres. Hvis systemet skal hente elementer på tværs af Activity, Task og Quiz, skal data samles fra flere tabeller. Microsofts dokumentation påpeger, at TPT ofte har dårligere query-performance end TPH, fordi forespørgsler over et hierarki kan kræve flere joins.

For QuestGame er det relevant, fordi kortvisningen netop er en forespørgsel, hvor vi er interesserede i alle QuestItems samlet. TPT giver et pænt skema, men kan ende med at optimere databasen efter klassestrukturen frem for de use cases, systemet faktisk skal understøtte.

TPC – en tabel pr. konkret type

TPC adskiller sig fra TPT ved, at der ikke oprettes en fælles tabel for den abstrakte basetype. Activity, Task og Quiz får hver sin komplette tabel, og de fælles properties fra QuestItem bliver gentaget i alle tre tabeller.

Fordelen er, at en forespørgsel efter én konkret type kan være effektiv, fordi alle data ligger i én tabel og ikke kræver joins. Microsoft beskriver TPC som særligt velegnet, når applikationen hovedsageligt forespørger på enkelte konkrete undertyper.

Ulempen viser sig, når typerne skal behandles samlet. Hvis vi vil hente alle QuestItems, skal data fra flere tabeller kombineres. Samtidig gentages de fælles kolonner, og relationer til basetypen bliver mere komplicerede, fordi der ikke findes én fælles tabel, som en foreign key kan pege på.

Vurdering i forhold til QuestGame

I vores nuværende model hælder jeg til TPH. Det afgørende argument er ikke blot, at det er EF Cores standard, men at strategien passer til vores konkrete adgangsmønster.

Når en spiller åbner en QuestSession, skal kortet kunne vise alle QuestItems. Backend skal i første omgang kun returnere den nødvendige kortinformation, eksempelvis PositionId, koordinater, type og status. Herefter kan de fulde oplysninger om den konkrete Activity, Task eller Quiz hentes, når spilleren interagerer med positionen.

Det er også vigtigt, at kortets status ikke automatisk betyder, at status skal lagres direkte på QuestItem. Status kan være afhængig af den konkrete QuestSession og spillerens progression og kan derfor være resultatet af en forespørgsel på eksempelvis submissions eller sessiondata. Backend kan samle dette i den DTO, der sendes til kortet.

Det betyder, at vi både har et fælles domænebegreb og et fælles query-behov. Activity, Task og Quiz skal ofte kunne behandles som QuestItem, uden at klienten behøver kende alle deres typespecifikke egenskaber. Her passer TPH naturligt.

Valget er dog ikke endeligt. Hvis undertyperne senere får mange forskellige properties, eller hvis vi finder på at tilføje flere undertyper, vil vi genoverveje strategien. Indtil videre synes jeg dog at TPH giver mest mening.

Kobling til mit læringsmål

Valget af inheritance mapping har en sammenhæng med mit læringsmål inden for databaser og datalagring. Målet er blandt andet, at jeg skal kunne analysere projektets databehov og træffe begrundede valg om datamodellering, relationer, dataintegritet og dataadgang samt integrere løsningen med backend gennem EF Core.

Sammenligningen af TPH, TPT og TPC er derfor ikke kun et spørgsmål om, hvordan C#-klasser gemmes i databasen. Det handler om at tage udgangspunkt i QuestGames use cases og vurdere, hvordan data faktisk skal anvendes. Kortvisningen giver et konkret eksempel på, at valg af databasestruktur, EF Core-mapping og backendens forespørgsler hænger tæt sammen.

Mit foreløbige valg er derfor TPH til QuestItem-hierarkiet, fordi det giver en enkel model for de fælles forespørgsler, som QuestGame aktuelt kræver. Samtidig er det et valg, der kan dokumenteres, testes og senere revurderes, hvis projektets datamodel eller adgangsmønstre ændrer sig.

Kilder