Dziedziczenie
Program EF może mapować hierarchię typów platformy .NET na bazę danych. Umożliwia to pisanie jednostek platformy .NET w kodzie w zwykły sposób, przy użyciu typów podstawowych i pochodnych oraz bezproblemowe tworzenie odpowiedniego schematu bazy danych, wystawianie zapytań itp. Rzeczywiste szczegóły sposobu mapowania hierarchii typów są zależne od dostawcy; Na tej stronie opisano obsługę dziedziczenia w kontekście relacyjnej bazy danych.
Mapowanie hierarchii typów jednostek
Zgodnie z konwencją program EF nie będzie automatycznie skanował pod kątem typów bazowych ani pochodnych; Oznacza to, że jeśli chcesz, aby typ CLR w hierarchii był mapowany, musisz jawnie określić ten typ w modelu. Na przykład określenie tylko podstawowego typu hierarchii nie spowoduje, że program EF Core niejawnie uwzględni wszystkie jego podtypy.
Poniższy przykład uwidacznia element DbSet dla Blog
i jego podklasę RssBlog
. Jeśli Blog
ma inną podklasę, nie zostanie ona uwzględniona w modelu.
internal class MyContext : DbContext
{
public DbSet<Blog> Blogs { get; set; }
public DbSet<RssBlog> RssBlogs { get; set; }
}
public class Blog
{
public int BlogId { get; set; }
public string Url { get; set; }
}
public class RssBlog : Blog
{
public string RssUrl { get; set; }
}
Uwaga
Kolumny bazy danych są automatycznie dopuszczane do wartości null w razie potrzeby podczas korzystania z mapowania TPH. Na przykład kolumna RssUrl
jest dopuszczana do wartości null, ponieważ zwykłe Blog
wystąpienia nie mają tej właściwości.
Jeśli nie chcesz uwidaczniać DbSet
elementu dla co najmniej jednej jednostki w hierarchii, możesz również użyć interfejsu API Fluent, aby upewnić się, że są one uwzględnione w modelu.
Napiwek
Jeśli nie korzystasz z konwencji, możesz jawnie określić typ podstawowy przy użyciu polecenia HasBaseType
. Można również użyć .HasBaseType((Type)null)
polecenia , aby usunąć typ jednostki z hierarchii.
Konfiguracja tabeli na hierarchię i dyskryminator
Domyślnie program EF mapuje dziedziczenie przy użyciu wzorca TPH (table-per-hierarchy ). Funkcja TPH używa pojedynczej tabeli do przechowywania danych dla wszystkich typów w hierarchii, a dyskryminująca kolumna służy do identyfikowania typu reprezentowanego przez każdy wiersz.
Powyższy model jest mapowany na następujący schemat bazy danych (zwróć uwagę na niejawnie utworzoną Discriminator
kolumnę, która identyfikuje, który typ Blog
jest przechowywany w każdym wierszu).
Możesz skonfigurować nazwę i typ kolumny dyskryminującej oraz wartości, które są używane do identyfikowania każdego typu w hierarchii:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.HasDiscriminator<string>("blog_type")
.HasValue<Blog>("blog_base")
.HasValue<RssBlog>("blog_rss");
}
W powyższych przykładach program EF dodał dyskryminujące niejawnie jako właściwość w tle w podstawowej jednostce hierarchii. Tę właściwość można skonfigurować tak jak każda inna:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.Property("Discriminator")
.HasMaxLength(200);
}
Na koniec dyskryminujące można również zamapować na zwykłą właściwość platformy .NET w jednostce:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.HasDiscriminator(b => b.BlogType);
modelBuilder.Entity<Blog>()
.Property(e => e.BlogType)
.HasMaxLength(200)
.HasColumnName("blog_type");
modelBuilder.Entity<RssBlog>();
}
Podczas wykonywania zapytań dotyczących jednostek pochodnych, które używają wzorca TPH, program EF Core dodaje predykat do kolumny dyskryminującej w zapytaniu. Ten filtr zapewnia, że nie otrzymujemy żadnych dodatkowych wierszy dla typów podstawowych ani typów elementów równorzędnych, które nie są wynikiem. Ten predykat filtru jest pomijany dla typu jednostki podstawowej, ponieważ wykonywanie zapytań dotyczących jednostki bazowej spowoduje uzyskanie wyników dla wszystkich jednostek w hierarchii. Podczas materializowania wyników zapytania, jeśli napotkamy dyskryminującą wartość, która nie jest mapowana na żaden typ jednostki w modelu, zgłaszamy wyjątek, ponieważ nie wiemy, jak zmaterializować wyniki. Ten błąd występuje tylko wtedy, gdy baza danych zawiera wiersze z wartościami dyskryminującymi, które nie są mapowane w modelu EF. Jeśli masz takie dane, możesz oznaczyć mapowanie dyskryminujące w modelu EF Core jako niekompletne, aby wskazać, że zawsze powinniśmy dodać predykat filtru do wykonywania zapytań o dowolny typ w hierarchii. IsComplete(false)
wzywanie do dyskryminującej konfiguracji oznacza, że mapowanie jest niekompletne.
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.HasDiscriminator()
.IsComplete(false);
}
Kolumny udostępnione
Domyślnie, gdy dwa typy jednostek równorzędnych w hierarchii mają właściwość o tej samej nazwie, zostaną one zamapowane na dwie oddzielne kolumny. Jeśli jednak ich typ jest identyczny, można je zamapować na tę samą kolumnę bazy danych:
public class MyContext : DbContext
{
public DbSet<BlogBase> Blogs { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.Property(b => b.Url)
.HasColumnName("Url");
modelBuilder.Entity<RssBlog>()
.Property(b => b.Url)
.HasColumnName("Url");
}
}
public abstract class BlogBase
{
public int BlogId { get; set; }
}
public class Blog : BlogBase
{
public string Url { get; set; }
}
public class RssBlog : BlogBase
{
public string Url { get; set; }
}
Uwaga
Dostawcy relacyjnej bazy danych, tacy jak PROGRAM SQL Server, nie będą automatycznie używać dyskryminującego predykatu podczas wykonywania zapytań dotyczących kolumn udostępnionych podczas korzystania z rzutowania. Zapytanie Url = (blog as RssBlog).Url
zwróci Url
również wartość wierszy elementów równorzędnych Blog
. Aby ograniczyć zapytanie do RssBlog
jednostek, należy ręcznie dodać filtr dyskryminujący, taki jak Url = blog is RssBlog ? (blog as RssBlog).Url : null
.
Konfiguracja tabeli na typ
We wzorcu mapowania TPT wszystkie typy są mapowane na poszczególne tabele. Właściwości należące wyłącznie do typu podstawowego lub pochodnego są przechowywane w tabeli, która jest mapowana na ten typ. Tabele mapujące na typy pochodne przechowują również klucz obcy, który łączy tabelę pochodną z tabelą bazową.
modelBuilder.Entity<Blog>().ToTable("Blogs");
modelBuilder.Entity<RssBlog>().ToTable("RssBlogs");
Napiwek
Zamiast wywoływać ToTable
każdy typ jednostki, można wywołać modelBuilder.Entity<Blog>().UseTptMappingStrategy()
dla każdego typu jednostki głównej, a nazwy tabel będą generowane przez platformę EF.
Napiwek
Aby skonfigurować różne nazwy kolumn dla kolumn klucza podstawowego w każdej tabeli, zobacz Konfiguracja aspektów specyficznych dla tabeli.
Program EF utworzy następujący schemat bazy danych dla powyższego modelu.
CREATE TABLE [Blogs] (
[BlogId] int NOT NULL IDENTITY,
[Url] nvarchar(max) NULL,
CONSTRAINT [PK_Blogs] PRIMARY KEY ([BlogId])
);
CREATE TABLE [RssBlogs] (
[BlogId] int NOT NULL,
[RssUrl] nvarchar(max) NULL,
CONSTRAINT [PK_RssBlogs] PRIMARY KEY ([BlogId]),
CONSTRAINT [FK_RssBlogs_Blogs_BlogId] FOREIGN KEY ([BlogId]) REFERENCES [Blogs] ([BlogId]) ON DELETE NO ACTION
);
Uwaga
Jeśli ograniczenie klucza podstawowego zostanie zmienione, nowa nazwa zostanie zastosowana do wszystkich tabel zamapowanych na hierarchię, przyszłe wersje programu EF będą zezwalać na zmianę nazwy ograniczenia tylko dla określonej tabeli, gdy problem 19970 zostanie rozwiązany.
Jeśli używasz konfiguracji zbiorczej, możesz pobrać nazwę kolumny dla określonej tabeli, wywołując metodę GetColumnName(IProperty, StoreObjectIdentifier).
foreach (var entityType in modelBuilder.Model.GetEntityTypes())
{
var tableIdentifier = StoreObjectIdentifier.Create(entityType, StoreObjectType.Table);
Console.WriteLine($"{entityType.DisplayName()}\t\t{tableIdentifier}");
Console.WriteLine(" Property\tColumn");
foreach (var property in entityType.GetProperties())
{
var columnName = property.GetColumnName(tableIdentifier.Value);
Console.WriteLine($" {property.Name,-10}\t{columnName}");
}
Console.WriteLine();
}
Ostrzeżenie
W wielu przypadkach TPT pokazuje niższą wydajność w porównaniu z TPH. Aby uzyskać więcej informacji, zobacz dokumentację dotyczącą wydajności.
Uwaga
Kolumny dla typu pochodnego są mapowane na różne tabele, dlatego w bazie danych nie można utworzyć złożonych ograniczeń i indeksów szyfrowania FK używających zarówno dziedziczonej, jak i zadeklarowanej właściwości.
Konfiguracja tabeli na konkretny typ
Uwaga
Funkcja TPC (table-per-concrete-type) została wprowadzona w programie EF Core 7.0.
We wzorcu mapowania TPC wszystkie typy są mapowane na poszczególne tabele. Każda tabela zawiera kolumny dla wszystkich właściwości odpowiedniego typu jednostki. Rozwiązuje to niektóre typowe problemy z wydajnością strategii TPT.
Napiwek
Zespół EF pokazał i szczegółowo omówił mapowanie TPC w odcinku standupu społeczności danych platformy .NET. Podobnie jak w przypadku wszystkich odcinków Community Standup, możesz obejrzeć odcinek TPC teraz na YouTube.
modelBuilder.Entity<Blog>().UseTpcMappingStrategy()
.ToTable("Blogs");
modelBuilder.Entity<RssBlog>()
.ToTable("RssBlogs");
Napiwek
Zamiast wywoływać ToTable
dla każdego typu jednostki tylko wywołanie modelBuilder.Entity<Blog>().UseTpcMappingStrategy()
dla każdego typu jednostki głównej spowoduje wygenerowanie nazw tabel według konwencji.
Napiwek
Aby skonfigurować różne nazwy kolumn dla kolumn klucza podstawowego w każdej tabeli, zobacz Konfiguracja aspektów specyficznych dla tabeli.
Program EF utworzy następujący schemat bazy danych dla powyższego modelu.
CREATE TABLE [Blogs] (
[BlogId] int NOT NULL DEFAULT (NEXT VALUE FOR [BlogSequence]),
[Url] nvarchar(max) NULL,
CONSTRAINT [PK_Blogs] PRIMARY KEY ([BlogId])
);
CREATE TABLE [RssBlogs] (
[BlogId] int NOT NULL DEFAULT (NEXT VALUE FOR [BlogSequence]),
[Url] nvarchar(max) NULL,
[RssUrl] nvarchar(max) NULL,
CONSTRAINT [PK_RssBlogs] PRIMARY KEY ([BlogId])
);
Schemat bazy danych TPC
Strategia TPC jest podobna do strategii TPT, z tą różnicą, że dla każdego konkretnego typu w hierarchii tworzona jest inna tabela, ale tabele nie są tworzone dla typów abstrakcyjnych — stąd nazwa "tabela-typ-beton". Podobnie jak w przypadku TPT, sama tabela wskazuje typ zapisanego obiektu. Jednak w przeciwieństwie do mapowania TPT każda tabela zawiera kolumny dla każdej właściwości w typie betonowym i jego typach podstawowych. Schematy bazy danych TPC są zdenormalizowane.
Rozważ na przykład mapowanie tej hierarchii:
public abstract class Animal
{
protected Animal(string name)
{
Name = name;
}
public int Id { get; set; }
public string Name { get; set; }
public abstract string Species { get; }
public Food? Food { get; set; }
}
public abstract class Pet : Animal
{
protected Pet(string name)
: base(name)
{
}
public string? Vet { get; set; }
public ICollection<Human> Humans { get; } = new List<Human>();
}
public class FarmAnimal : Animal
{
public FarmAnimal(string name, string species)
: base(name)
{
Species = species;
}
public override string Species { get; }
[Precision(18, 2)]
public decimal Value { get; set; }
public override string ToString()
=> $"Farm animal '{Name}' ({Species}/{Id}) worth {Value:C} eats {Food?.ToString() ?? "<Unknown>"}";
}
public class Cat : Pet
{
public Cat(string name, string educationLevel)
: base(name)
{
EducationLevel = educationLevel;
}
public string EducationLevel { get; set; }
public override string Species => "Felis catus";
public override string ToString()
=> $"Cat '{Name}' ({Species}/{Id}) with education '{EducationLevel}' eats {Food?.ToString() ?? "<Unknown>"}";
}
public class Dog : Pet
{
public Dog(string name, string favoriteToy)
: base(name)
{
FavoriteToy = favoriteToy;
}
public string FavoriteToy { get; set; }
public override string Species => "Canis familiaris";
public override string ToString()
=> $"Dog '{Name}' ({Species}/{Id}) with favorite toy '{FavoriteToy}' eats {Food?.ToString() ?? "<Unknown>"}";
}
public class Human : Animal
{
public Human(string name)
: base(name)
{
}
public override string Species => "Homo sapiens";
public Animal? FavoriteAnimal { get; set; }
public ICollection<Pet> Pets { get; } = new List<Pet>();
public override string ToString()
=> $"Human '{Name}' ({Species}/{Id}) with favorite animal '{FavoriteAnimal?.Name ?? "<Unknown>"}'" +
$" eats {Food?.ToString() ?? "<Unknown>"}";
}
W przypadku korzystania z programu SQL Server tabele utworzone dla tej hierarchii to:
CREATE TABLE [Cats] (
[Id] int NOT NULL DEFAULT (NEXT VALUE FOR [AnimalSequence]),
[Name] nvarchar(max) NOT NULL,
[FoodId] uniqueidentifier NULL,
[Vet] nvarchar(max) NULL,
[EducationLevel] nvarchar(max) NOT NULL,
CONSTRAINT [PK_Cats] PRIMARY KEY ([Id]));
CREATE TABLE [Dogs] (
[Id] int NOT NULL DEFAULT (NEXT VALUE FOR [AnimalSequence]),
[Name] nvarchar(max) NOT NULL,
[FoodId] uniqueidentifier NULL,
[Vet] nvarchar(max) NULL,
[FavoriteToy] nvarchar(max) NOT NULL,
CONSTRAINT [PK_Dogs] PRIMARY KEY ([Id]));
CREATE TABLE [FarmAnimals] (
[Id] int NOT NULL DEFAULT (NEXT VALUE FOR [AnimalSequence]),
[Name] nvarchar(max) NOT NULL,
[FoodId] uniqueidentifier NULL,
[Value] decimal(18,2) NOT NULL,
[Species] nvarchar(max) NOT NULL,
CONSTRAINT [PK_FarmAnimals] PRIMARY KEY ([Id]));
CREATE TABLE [Humans] (
[Id] int NOT NULL DEFAULT (NEXT VALUE FOR [AnimalSequence]),
[Name] nvarchar(max) NOT NULL,
[FoodId] uniqueidentifier NULL,
[FavoriteAnimalId] int NULL,
CONSTRAINT [PK_Humans] PRIMARY KEY ([Id]));
Zwróć uwagę, że:
Nie ma tabel dla
Animal
typów lubPet
, ponieważ znajdują sięabstract
one w modelu obiektów. Należy pamiętać, że język C# nie zezwala na wystąpienia typów abstrakcyjnych i dlatego nie ma sytuacji, w której wystąpienie typu abstrakcyjnego zostanie zapisane w bazie danych.Mapowanie właściwości w typach podstawowych jest powtarzane dla każdego konkretnego typu. Na przykład każda tabela ma kolumnę
Name
, a zarówno Koty, jak i Psy mają kolumnęVet
.Zapisanie niektórych danych w tej bazie danych powoduje wykonanie następujących czynności:
Tabela Cats
Id | Nazwisko | Identyfikator żywności | Vet | EducationLevel |
---|---|---|---|---|
1 | Alicja | 99ca3e98-b26d-4a0c-d4ae-08da7aca624f | Pengelly | MBA |
2 | Mac | 99ca3e98-b26d-4a0c-d4ae-08da7aca624f | Pengelly | Wieku przedszkolnym |
8 | Baxter | 5dc5019e-6f72-454b-d4b0-08da7aca624f | Bothell Pet Hospital | Bsc |
Stół dla psów
Id | Nazwisko | Identyfikator żywności | Vet | UlubioneToy |
---|---|---|---|---|
3 | Toast | 011aaf6f-d588-4fad-d4ac-08da7aca624f | Pengelly | Pan Wiewiórka |
Tabela FarmAnimals
Id | Nazwisko | Identyfikator żywności | Wartość | Gatunki |
---|---|---|---|---|
4 | Clyde | 1d495075-f527-4498-d4af-08da7aca624f | 100.00 | Equus africanus asinus |
Stół dla ludzi
Id | Nazwisko | Identyfikator żywności | FavoriteAnimalId |
---|---|---|---|
5 | Wendy | 5418fd81-7660-432f-d4b1-08da7aca624f | 2 |
6 | Arthur | 59b495d4-0414-46bf-d4ad-08da7aca624f | 1 |
9 | Katie | null | 8 |
Zwróć uwagę, że w przeciwieństwie do mapowania TPT wszystkie informacje dotyczące pojedynczego obiektu znajdują się w jednej tabeli. I, w przeciwieństwie do mapowania TPH, nie ma kombinacji kolumn i wierszy w żadnej tabeli, w której nigdy nie jest używany przez model. Poniżej zobaczymy, jak te cechy mogą być ważne dla zapytań i magazynu.
Generowanie klucza
Wybrana strategia mapowania dziedziczenia ma konsekwencje dla sposobu generowania i zarządzania wartościami klucza podstawowego. Klucze w TPH są łatwe, ponieważ każde wystąpienie jednostki jest reprezentowane przez jeden wiersz w jednej tabeli. Można użyć dowolnego rodzaju generowania wartości klucza i nie są potrzebne żadne dodatkowe ograniczenia.
W przypadku strategii TPT zawsze istnieje wiersz w tabeli zamapowany na podstawowy typ hierarchii. W tym wierszu można używać dowolnego rodzaju generowania kluczy, a klucze dla innych tabel są połączone z tą tabelą przy użyciu ograniczeń klucza obcego.
Rzeczy są nieco bardziej skomplikowane dla TPC. Najpierw należy pamiętać, że platforma EF Core wymaga, aby wszystkie jednostki w hierarchii miały unikatową wartość klucza, nawet jeśli jednostki mają różne typy. Na przykład przy użyciu naszego przykładowego modelu pies nie może mieć takiej samej wartości klucza identyfikatora, jak kot. Po drugie, w przeciwieństwie do TPT, nie ma wspólnej tabeli, która może działać jako pojedyncze miejsce, w którym żyją wartości kluczy i można je wygenerować. Oznacza to, że nie można użyć prostej Identity
kolumny.
W przypadku baz danych obsługujących sekwencje wartości kluczy można wygenerować przy użyciu pojedynczej sekwencji przywoływanej w domyślnym ograniczeniu dla każdej tabeli. Jest to strategia używana w tabelach TPC przedstawionych powyżej, gdzie każda tabela ma następujące elementy:
[Id] int NOT NULL DEFAULT (NEXT VALUE FOR [AnimalSequence])
AnimalSequence
to sekwencja bazy danych utworzona przez program EF Core. Ta strategia jest używana domyślnie w przypadku hierarchii TPC podczas korzystania z dostawcy bazy danych EF Core dla programu SQL Server. Dostawcy baz danych dla innych baz danych obsługujących sekwencje powinny mieć podobną wartość domyślną. Inne strategie generowania kluczy, które używają sekwencji, takich jak wzorce Hi-Lo, mogą być również używane z TPC.
Chociaż standardowe kolumny tożsamości nie działają z TPC, można użyć kolumn identity, jeśli każda tabela jest skonfigurowana z odpowiednim inicjatorem i przyrostem, tak aby wartości wygenerowane dla każdej tabeli nigdy nie powodować konfliktu. Przykład:
modelBuilder.Entity<Cat>().ToTable("Cats", tb => tb.Property(e => e.Id).UseIdentityColumn(1, 4));
modelBuilder.Entity<Dog>().ToTable("Dogs", tb => tb.Property(e => e.Id).UseIdentityColumn(2, 4));
modelBuilder.Entity<FarmAnimal>().ToTable("FarmAnimals", tb => tb.Property(e => e.Id).UseIdentityColumn(3, 4));
modelBuilder.Entity<Human>().ToTable("Humans", tb => tb.Property(e => e.Id).UseIdentityColumn(4, 4));
Ważne
Użycie tej strategii utrudnia dodawanie typów pochodnych później, ponieważ wymaga, aby łączna liczba typów w hierarchii była wcześniej znana.
SqLite nie obsługuje sekwencji ani inkrementacji tożsamości, dlatego generowanie wartości klucza całkowitego nie jest obsługiwane w przypadku używania biblioteki SQLite ze strategią TPC. Jednak generowanie po stronie klienta lub globalnie unikatowe klucze — takie jak identyfikatory GUID — są obsługiwane w dowolnej bazie danych, w tym sqlite.
Ograniczenia klucza obcego
Strategia mapowania TPC tworzy zdenormalizowany schemat SQL — jest to jeden z powodów, dla których niektórzy puryści bazy danych są przeciwko nim. Rozważmy na przykład kolumnę FavoriteAnimalId
klucza obcego . Wartość w tej kolumnie musi być zgodna z wartością klucza podstawowego niektórych zwierząt. Można to wymusić w bazie danych z prostym ograniczeniem FK podczas korzystania z TPH lub TPT. Przykład:
CONSTRAINT [FK_Animals_Animals_FavoriteAnimalId] FOREIGN KEY ([FavoriteAnimalId]) REFERENCES [Animals] ([Id])
Jednak w przypadku korzystania z TPC klucz podstawowy dla danego zwierzęcia jest przechowywany w tabeli odpowiadającej konkretnemu typowi tego zwierzęcia. Na przykład klucz podstawowy kota jest przechowywany w Cats.Id
kolumnie, podczas gdy klucz podstawowy psa jest przechowywany w Dogs.Id
kolumnie itd. Oznacza to, że nie można utworzyć ograniczenia FK dla tej relacji.
W praktyce nie jest to problem, o ile aplikacja nie próbuje wstawić nieprawidłowych danych. Jeśli na przykład wszystkie dane są wstawione przez program EF Core i używają nawigacji do powiązania jednostek, gwarantowana jest, że kolumna FK będzie zawierać prawidłowe wartości PK przez cały czas.
Podsumowanie i wskazówki
Podsumowując, funkcja TPH jest zwykle odpowiednia dla większości aplikacji i jest dobrym ustawieniem domyślnym dla szerokiego zakresu scenariuszy, więc nie dodawaj złożoności TPC, jeśli nie potrzebujesz. W szczególności jeśli kod będzie w większości wykonywać zapytania dotyczące jednostek wielu typów, takich jak pisanie zapytań względem typu podstawowego, pochylić się w kierunku TPH przez TPC.
Oznacza to, że TPC jest również dobrą strategią mapowania, która ma być używana, gdy kod będzie głównie wysyłać zapytania o jednostki pojedynczego typu liścia, a testy porównawcze pokazują poprawę w porównaniu z TPH.
Należy użyć TPT tylko wtedy, gdy jest to ograniczone przez czynniki zewnętrzne.