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).

Screenshot of the results of querying the Blog entity hierarchy using table-per-hierarchy pattern

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 nietworzone 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 lub Pet , 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ę FavoriteAnimalIdklucza 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.