Modelowanie relacji - dziedziczenie - EF Core

Mateusz Gajda12/31/2018 - 2 min read

Photo by: Agung Pratamah

Entity Framework dostarcza nam bardzo wygodne rozwiązanie do generowania migracji za pomocą obiektów które zdefiniujemy w naszym kodzie. Podejście code-first w temacie migracji pozwala nam ominąć cały proces pisania każdorazowo skryptu SQL w momencie potrzeby utworzenia nowej tabeli dla naszego modelu. Upraszcza on również proces modelowania relacji między obiektami. O ile w przypadku prostych modeli i ich relacji sprawa jest trywialna, o tyle w momencie kiedy nasze obiekty budowane są na zasadzie dziedziczenia, mieliśmy do wyboru trzy rozwiązania:

  • Table per Hierarchy (TPH) - podejście to zakłada stworzenia jednej, wspólnej tabeli dla wszystkich dziedziczonych encji. Tabela zawiera w sobie discriminator, który definiuje dokładny typ klasy, zapisanej w danym wierszu. Jest to domyślne podejście w definiowaniu tego typu relacji w EF. I jedyne dostępne w EF Core.
  • Table per Type (TPT) - w tym przypadku, jak sama nazwa wskazuje zakładamy że każda klasa (również abstrakcyjna), ma swoją odrębne reprezentację jako tabela w bazie danych. Nie istnieją więc żadne relację między tabelami.
  • Table per Concrete Class (TPC) - jedna tabela, per klasa, W przypadku klas abstrakcyjnych, ich właściwości będą znajdowały się w każdej tabeli klas po niej dziedziczących.

Jako że najlepiej uczy się w praktyce, przyjmijmy więc że potrzebujemy stworzyć, na potrzeby naszej aplikacji, modele odnoszące się do dwóch rodzajów wydarzeń. Jeden z nich ma reprezentować wydarzenia płatne, drugi darmowe. Na początku możemy stworzyć klasę bazową Event, która będzie klasą abstrakcyjną, jako że nie chcemy tworzyć żadnych innych wydarzeń, a zdecydowanie nie chcemy by wydarzenie nie było inne niż dwa poprzednie.

public abstract class Event
{
public int EventId { get; set; }
public DateTime StartDate { get; set; }
public DateTime EndDate { get; set; }
public int Seats { get; set; }
}

Następnie tworzymy dwa obiekty dziedziczące o których wspomnieliśmy wcześniej:

public class PaidEvent : Event
{
public decimal Price { get; set; }
}
public class FreeEvent : Event
{
public string SponsorName { get; set; }
}

Klasy te wiele się od siebie nie różnią. Pierwsza zawiera cenę, którą trzeba zapłacić aby się na owe wydarzenie wybrać. Druga uwzględnia nazwę sponsora, który wyłożył pieniądze po to, abyśmy my już nie musieli płacić :)

Table per Hierarchy (TPH)

Konfiguracja nie jest skomplikowana, dla naszego przypadku będzie ona wyglądać następująco:

public class EventConfiguration : IEntityTypeConfiguration<event>
{
public void Configure(EntityTypeBuilder<event> builder)
{
builder.ToTable(Events)
.HasDiscriminator<eventtype>(EventType)
.HasValue<freeevent>(EventType.Free)
.HasValue<paidevent>(EventType.Paid);
}
}

Najpierw definiujemy nazwę naszej tabeli, w tym przypadku Events. Następnie definiujemy typ dyskryminatora. Zdecydowałem się skorzystać z enuma, z tego względu że jest on łatwo rozszerzalny i czytelny. Zamiast niego możemy przykładowo wykorzystać typ int. Dodatkowo musimy określić nazwę kolumny, w które będzie się on znajdować. Ostatnią rzeczą którą musimy zdefiniować, to klasy, które mają znaleźć się w nowo utworzonej tabeli. Poprzez metodę HasValue(TDiscriminator value) definiujemy typ obiektu oraz przypisujemy mu wartość dyskryminatora, po którym będziemy go rozpoznawać. Efektem końcowym powinna być taka migracja:

migrationBuilder.CreateTable(
name: Events,
columns: table => new
{
EventId = table.Column<int>(nullable: false)
.Annotation(SqlServer:ValueGenerationStrategy, SqlServerValueGenerationStrategy.IdentityColumn),
StartDate = table.Column<datetime>(nullable: false),
EndDate = table.Column<datetime>(nullable: false),
Seats = table.Column<int>(nullable: false),
EventType = table.Column<int>(nullable: false),
SponsorName = table.Column<string>(nullable: true),
Price = table.Column<decimal>(nullable: true)
},
constraints: table =>
{
table.PrimaryKey(PK_Events, x => x.EventId);
});

Po wykonaniu powyższej migracji na naszej bazie danych, powinniśmy ujrzeć nową tabelę. Zawiera ona wszystkie kolumny, z dwóch obiektów które zdefiniowaliśmy w jej ramach.

Dzięki temu, w poniższy sposób możemy zapisywać i odczytywać dane z naszej bazy

using (var db = _dbFactory.CreateDbContext())
{
var freeEvent = new FreeEvent
{
StartDate = new DateTime(2019, 1, 1),
EndDate = new DateTime(2019, 1, 2),
Seats = 10,
SponsorName = Any Sponsor
};
var paidEvent = new PaidEvent
{
StartDate = new DateTime(2019, 1, 1),
EndDate = new DateTime(2019, 1, 2),
Seats = 12,
Price = 21
};
db.Add<event>(freeEvent);
db.Add<event>(paidEvent);
db.SaveChanges();
var events = db.Set<event>();
}

Table Per Type i Table Per Concrete Type

Jeżeli chodzi o te dwa typy relacji, to póki co Entity Framework Core ich nie wspiera. Znajdują się one jednak w backlogu, a ich status możemy śledzić tutaj dla TPT oraz tutaj dla TPC.

Warto jednak wspomnieć że mimo tego że oficjalnie TPT nie jest wspierane przez EF Core, możemy jednak z niego skorzystać zastępując dziedziczenie, kompozycją. Na swoim blogu Pawel Gerr\'s opisał w jaki sposób to osiągnąć. Warto jednak pamiętać, że tracimy przy tym część zalet płynących z polimorfizmu.

Podsumowując

W przeciwieństwie do EF6, Core obsługuje tylko jeden z trzech wcześniej dostępnych sposobów na zamodelowanie dziedziczenia w naszej bazie danych. Póki co wiadomo że TPT nie zostanie dołączony do wydania 3.0 i póki co nie jest znana konkretna data jego implementacji. Podobnie jest zresztą z TPC, jak możemy wyczytać w issue na githubie, również póki co ten feature nie wszedł w plan żadnego wydania. Pozostaje więc dalej czekać, a wszystkich ciekawych zapraszam do śledzenia issues tych dwóch features.