Udostępnij za pośrednictwem


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 ustawiane na NULL, jeśli jest to konieczne, podczas korzystania z mapowania TPH. Na przykład kolumna RssUrl może przyjmować wartość null, ponieważ zwykłe instancje Blog nie mają tej właściwości.

Jeśli nie chcesz eksponować DbSet dla jednego lub więcej elementów w hierarchii, możesz również użyć Fluent API, aby upewnić się, że są one uwzględnione w modelu.

Wskazówka

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 dla hierarchii i dyskryminatora

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

Zrzut ekranu przedstawiający wyniki wykonywania zapytań dotyczących hierarchii jednostki Blog przy użyciu wzorca tabela-per-hierarchia

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ł dyskryminator domyślnie jako ukrytą właściwością 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("blog_type")
        .HasMaxLength(200);
}

Na koniec dyskryminator można również zamapować na zwykłą właściwość .NET w encji.

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 pokrewnych, które nie są zawarte w wyniku. 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) wywołanie konfiguracji rozróżniającej 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 SQL Server, nie będą automatycznie używać predykatu dyskryminującego podczas wykonywania zapytań dotyczących kolumn udostępnionych podczas korzystania z rzutowania. Zapytanie Url = (blog as RssBlog).Url również zwróci wartość Url dla wierszy rodzeństwa 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 tabel 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");

Wskazówka

Zamiast wywoływać ToTable dla każdego typu jednostki, można wywołać modelBuilder.Entity<Blog>().UseTptMappingStrategy() dla każdego typu jednostki głównej, a nazwy tabel będą generowane przez EF.

Wskazówka

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.

Ostrożność

Kolumny dla typu pochodnego są mapowane na różne tabele, dlatego w bazie danych nie można utworzyć złożonych ograniczeń FK i indeksów, które używają zarówno dziedziczonych, jak i zadeklarowanych właściwości.

Konfiguracja tabeli na konkretny typ

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.

Wskazówka

Zespół EF zaprezentował i szczegółowo omówił mapowanie TPC w odcinku Standupu Społeczności Danych .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");

Wskazówka

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.

Wskazówka

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-na-konkretnego-typu". Podobnie jak w przypadku TPT, sama tabela wskazuje typ zapisanego obiektu. Jednak w przeciwieństwie do mapowania TPT, każda tabela zawiera kolumny dla wszystkich właściwości w typie konkretnym 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 typów Animal lub Pet, ponieważ są to abstract w modelu obiektowym." 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 Kotów

Id Nazwa Identyfikator żywności Weterynarz Poziom wykształcenia
1 Alicja 99ca3e98-b26d-4a0c-d4ae-08da7aca624f Pengelly MBA
2 Mac 99ca3e98-b26d-4a0c-d4ae-08da7aca624f Pengelly Przedszkole
8 Baxter 5dc5019e-6f72-454b-d4b0-08da7aca624f Bothell Pet Hospital Bsc

Stół dla psów

Id Nazwa Identyfikator żywności Weterynarz UlubioneToy
3 Grzanka 011aaf6f-d588-4fad-d4ac-08da7aca624f Pengelly Pan Wiewiórka

Tabela FarmAnimals

Id Nazwa Identyfikator żywności Wartość Gatunek
4 Clyde 1d495075-f527-4498-d4af-08da7aca624f 100,00 Equus africanus asinus (osioł domowy)

Stół dla ludzi

Id Nazwa Identyfikator żywności UlubioneZwierzęId
5 Wendy 5418fd81-7660-432f-d4b1-08da7aca624f 2
6 Arthur 59b495d4-0414-46bf-d4ad-08da7aca624f 1
9 Katie zero 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 istnieje żadna kombinacja kolumn i wierszy w żadnej tabeli, której model nigdy nie używa. Poniżej rozważymy, jak te cechy mogą być ważne dla zapytań i przechowywania.

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 znajdują się wartości kluczy i należy 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 wzory Hi-Lo, mogą być również używane wraz z TPC.

Nie można używać standardowych kolumn tożsamości z TPC, ale można użyć kolumn tożsamości, jeśli każda tabela jest skonfigurowana z odpowiednim inicjatorem i przyrostem, tak aby wartości wygenerowane dla każdej tabeli nigdy nie powodowały konfliktu. Na 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 unikalne klucze — takie jak identyfikatory GUID — są obsługiwane w każdej bazie danych, w tym również 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ę klucza obcego FavoriteAnimalId. Wartość w tej kolumnie musi być zgodna z wartością klucza podstawowego jakiegoś zwierzęcia. W bazie danych można to wymusić za pomocą prostego ograniczenia FK, korzystając z TPH lub TPT. Na 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, skłaniaj się ku TPH zamiast TPC.

To powiedziawszy, TPC jest również dobrą strategią mapowania do użycia, gdy kod w większości będzie wysyłać zapytania dotyczące jednostek pojedynczego typu liściowego, a testy porównawcze wykazują poprawę w porównaniu z TPH.

Należy użyć TPT tylko wtedy, gdy jest to ograniczone przez czynniki zewnętrzne.