Dědičnost

EF může mapovat hierarchii typů .NET na databázi. To vám umožní psát entity .NET v kódu obvyklým způsobem pomocí základních a odvozených typů a bez problémů vytvářet příslušné schéma databáze, vydávat dotazy atd. Skutečné podrobnosti o mapování hierarchie typů jsou závislé na poskytovateli; tato stránka popisuje podporu dědičnosti v kontextu relační databáze.

Mapování hierarchie typů entit

Podle konvence ef automaticky neskenuje základní nebo odvozené typy; to znamená, že pokud chcete v hierarchii namapovat typ CLR, musíte tento typ explicitně zadat ve svém modelu. Například zadání pouze základního typu hierarchie nezpůsobí implicitní zahrnutí všech jeho dílčích typů EF Core.

Následující ukázka zveřejňuje sadu DbSet pro Blog a její podtřídu RssBlog. Pokud Blog obsahuje jakoukoli jinou podtřídu, nebude součástí 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; }
}

Poznámka

Při použití mapování TPH se sloupce databáze automaticky dají podle potřeby nastavit jako null. Například sloupec je nullable, RssUrl protože běžné Blog instance nemají tuto vlastnost.

Pokud nechcete zveřejnit DbSet jednu nebo více entit v hierarchii, můžete také použít rozhraní Fluent API, abyste zajistili, že jsou součástí modelu.

Tip

Pokud se nespoléháte na konvence, můžete zadat základní typ explicitně pomocí HasBaseType. Můžete také použít .HasBaseType((Type)null) k odebrání typu entity z hierarchie.

Konfigurace tabulek podle hierarchie a diskriminace

Ef ve výchozím nastavení mapuje dědičnost pomocí vzoru TPH (table-per-hierarchy ). TPH používá jednu tabulku k ukládání dat pro všechny typy v hierarchii a k identifikaci typu, který představuje každý řádek, se používá diskriminující sloupec.

Výše uvedený model je mapován na následující schéma databáze (všimněte si implicitně vytvořeného DiscriminatorBlog sloupce, který identifikuje, který typ je uložen v každém řádku).

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

Můžete nakonfigurovat název a typ nediskriminačního sloupce a hodnoty, které se používají k identifikaci jednotlivých typů v hierarchii:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
        .HasDiscriminator<string>("blog_type")
        .HasValue<Blog>("blog_base")
        .HasValue<RssBlog>("blog_rss");
}

V příkladech výše ef přidal diskriminátor implicitně jako stínovou vlastnost základní entity hierarchie. Tuto vlastnost lze nakonfigurovat stejně jako jakoukoli jinou:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
        .Property("Discriminator")
        .HasMaxLength(200);
}

Diskriminátor lze také namapovat na běžnou vlastnost .NET ve vaší entitě:

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>();
}

Při dotazování na odvozené entity, které používají model TPH, EF Core přidá predikát přes nediskriminační sloupec v dotazu. Tento filtr zajistí, že ve výsledku nezístíme žádné další řádky pro základní typy nebo typy na stejné straně. Tento predikát filtru se vynechá pro základní typ entity, protože dotazování na základní entitu získá výsledky pro všechny entity v hierarchii. Pokud při materializaci výsledků z dotazu narazíme na diskriminující hodnotu, která není namapovaná na žádný typ entity v modelu, vyvoláme výjimku, protože nevíme, jak materializovat výsledky. K této chybě dochází pouze v případě, že databáze obsahuje řádky s nediskriminačními hodnotami, které nejsou mapovány v modelu EF. Pokud taková data máte, můžete označit diskriminující mapování v modelu EF Core jako neúplné a označit, že bychom měli vždy přidat predikát filtru pro dotazování libovolného typu v hierarchii. IsComplete(false) volat diskriminující konfiguraci označuje, že mapování je neúplné.

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
        .HasDiscriminator()
        .IsComplete(false);
}

Sdílené sloupce

Ve výchozím nastavení platí, že pokud dva typy entit na stejné straně v hierarchii mají vlastnost se stejným názvem, budou namapovány na dva samostatné sloupce. Pokud je ale jejich typ identický, můžete je namapovat na stejný sloupec databáze:

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; }
}

Poznámka

Poskytovatelé relačních databází, jako je SQL Server, nebudou při dotazování sdílených sloupců při použití přetypování automaticky používat nediskriminační predikát. Dotaz Url = (blog as RssBlog).Url by také vrátil Url hodnotu pro řádky na stejné straně Blog . Pokud chcete omezit dotaz na RssBlog entity, musíte ručně přidat filtr na diskriminátor, například Url = blog is RssBlog ? (blog as RssBlog).Url : null.

Konfigurace tabulky podle typu

Ve vzoru mapování TPT se všechny typy mapují na jednotlivé tabulky. Vlastnosti, které patří výhradně do základního nebo odvozeného typu, jsou uloženy v tabulce, která se mapuje na tento typ. Tabulky mapované na odvozené typy také ukládají cizí klíč, který spojuje odvozenou tabulku se základní tabulkou.

modelBuilder.Entity<Blog>().ToTable("Blogs");
modelBuilder.Entity<RssBlog>().ToTable("RssBlogs");

Tip

Místo volání ToTable na každý typ entity můžete volat modelBuilder.Entity<Blog>().UseTptMappingStrategy() každý typ kořenové entity a názvy tabulek se vygenerují ef.

Tip

Pokud chcete nakonfigurovat různé názvy sloupců primárního klíče v každé tabulce, podívejte se na konfiguraci omezující vlastnosti specifické pro tabulku.

EF vytvoří následující schéma databáze pro výše uvedený model.

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

Poznámka

Pokud je omezení primárního klíče přejmenováno, použije se nový název u všech tabulek mapovaných na hierarchii, budoucí verze EF umožní přejmenování omezení pouze pro určitou tabulku, když je problém 19970 opravený.

Pokud používáte hromadnou konfiguraci, můžete načíst název sloupce pro konkrétní tabulku voláním 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();
}

Upozorňující

V mnoha případech tpT zobrazuje nižší výkon v porovnání s TPH. Další informace najdete v dokumentaci k výkonu.

Upozornění

Sloupce odvozeného typu se mapují na různé tabulky, proto složená omezení FK a indexy, které používají zděděné i deklarované vlastnosti, nelze v databázi vytvořit.

Konfigurace typu tabulka na konkrétní typ

Poznámka

Funkce TPC (table-per-beton-type) byla zavedena v EF Core 7.0.

Ve vzoru mapování TPC se všechny typy mapují na jednotlivé tabulky. Každá tabulka obsahuje sloupce pro všechny vlastnosti odpovídajícího typu entity. Řeší se tím některé běžné problémy s výkonem strategie TPT.

Tip

Ef Team demonstroval a podrobně mluvil o mapování TPC v epizodě standupu komunity dat .NET. Stejně jako u všech epizod Community Standup můžete sledovat epizodu TPC nyní na YouTube.

modelBuilder.Entity<Blog>().UseTpcMappingStrategy()
    .ToTable("Blogs");
modelBuilder.Entity<RssBlog>()
    .ToTable("RssBlogs");

Tip

Místo volání ToTable na každý typ entity, jen volání modelBuilder.Entity<Blog>().UseTpcMappingStrategy() na každý typ kořenové entity vygeneruje názvy tabulek podle konvence.

Tip

Pokud chcete nakonfigurovat různé názvy sloupců primárního klíče v každé tabulce, podívejte se na konfiguraci omezující vlastnosti specifické pro tabulku.

EF vytvoří následující schéma databáze pro výše uvedený model.

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])
);

Schéma databáze TPC

Strategie TPC je podobná strategii TPT s tím rozdílem, že pro každý konkrétní typ v hierarchii se vytvoří jiná tabulka, ale tabulky se nevytvořípro abstraktní typy – proto název "table-per-beton-type". Stejně jako u TPT označuje samotná tabulka typ uloženého objektu. Na rozdíl od mapování TPT však každá tabulka obsahuje sloupce pro každou vlastnost konkrétního typu a jeho základní typy. Schémata databáze TPC jsou denormalizovaná.

Zvažte například mapování této hierarchie:

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>"}";
}

Při použití SQL Serveru jsou tabulky vytvořené pro tuto hierarchii následující:

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]));

Všimněte si, že:

  • Neexistují žádné tabulky pro tyto Animal typy Pet , protože jsou abstract v objektovém modelu. Mějte na paměti, že jazyk C# neumožňuje instance abstraktních typů, a proto neexistuje situace, kdy se do databáze uloží instance abstraktního typu.

  • Mapování vlastností v základních typech se opakuje pro každý konkrétní typ. Každá tabulka má Name například sloupec a kočky i psi mají Vet sloupec.

  • Uložením některých dat do této databáze vznikne následující:

Tabulka kočky

Id Jméno FoodId Veterán EducationLevel
1 Alice 99ca3e98-b26d-4a0c-d4ae-08da7aca624f Pengelly MBA
2 Mac 99ca3e98-b26d-4a0c-d4ae-08da7aca624f Pengelly Školka
8 Baxter 5dc5019e-6f72-454b-d4b0-08da7aca624f Bothell Pet Hospital Bsc

Tabulka Psi

Id Jméno FoodId Veterán FavoriteToy
3 Toast 011aaf6f-d588-4fad-d4ac-08da7aca624f Pengelly Pan Veverka

Tabulka FarmAnimals

Id Jméno FoodId Hodnota Druh
4 Clyde 1d495075-f527-4498-d4af-08da7aca624f 100.00 Equus africanus asinus

Tabulka Lidé

Id Jméno FoodId FavoriteAnimalId
5 Wendy 5418fd81-7660-432f-d4b1-08da7aca624f 2
6 Arthur 59b495d4-0414-46bf-d4ad-08da7aca624f 1
9 Katie null 8

Všimněte si, že na rozdíl od mapování TPT jsou všechny informace pro jeden objekt obsaženy v jedné tabulce. A na rozdíl od mapování TPH neexistuje žádná kombinace sloupců a řádků v žádné tabulce, ve které model nikdy nepoužívá. Níže uvidíme, jak můžou být tyto charakteristiky pro dotazy a úložiště důležité.

Generování klíčů

Zvolená strategie mapování dědičnosti má důsledky pro generování a správu hodnot primárního klíče. Klíče v TPH jsou jednoduché, protože každá instance entity je reprezentována jedním řádkem v jedné tabulce. Je možné použít jakékoli generování hodnot klíče a nejsou potřeba žádná další omezení.

Pro strategii TPT existuje vždy řádek v tabulce namapovaný na základní typ hierarchie. Na tomto řádku lze použít jakékoli generování klíčů a klíče pro jiné tabulky jsou propojeny s touto tabulkou pomocí omezení cizího klíče.

Věci jsou pro TPC trochu složitější. Nejprve je důležité pochopit, že EF Core vyžaduje, aby všechny entity v hierarchii měly jedinečnou hodnotu klíče, i když entity mají různé typy. Například při použití našeho ukázkového modelu nemůže pes mít stejnou hodnotu klíče ID jako kočka. Zadruhé, na rozdíl od TPT neexistuje žádná běžná tabulka, která může fungovat jako jediné místo, kde jsou hodnoty klíčů živé a lze je vygenerovat. To znamená, že nelze použít jednoduchý Identity sloupec.

U databází, které podporují sekvence, je možné hodnoty klíčů vygenerovat pomocí jediné sekvence odkazované ve výchozím omezení pro každou tabulku. Toto je strategie použitá v tabulkách TPC uvedených výše, kde každá tabulka obsahuje následující:

[Id] int NOT NULL DEFAULT (NEXT VALUE FOR [AnimalSequence])

AnimalSequence je sekvence databáze vytvořená ef Core. Tato strategie se ve výchozím nastavení používá pro hierarchie TPC při použití poskytovatele databáze EF Core pro SQL Server. Poskytovatelé databází pro jiné databáze, které podporují sekvence, by měly mít podobné výchozí nastavení. Další strategie generování klíčů, které používají sekvence, jako jsou vzory Hi-Lo, mohou být také použity s TPC.

I když standardní sloupce identity nefungují s TPC, je možné použít sloupce Identity, pokud je každá tabulka nakonfigurovaná s odpovídajícím počátečním a přírůstkem tak, aby hodnoty vygenerované pro každou tabulku nikdy kolidovaly. Příklad:

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

Důležité

Použití této strategie znesnadňuje pozdější přidání odvozených typů, protože vyžaduje, aby byl předem znám celkový počet typů v hierarchii.

SQLite nepodporuje sekvence ani počáteční nebo přírůstkové hodnoty identity, a proto generování celočíselných hodnot klíče není podporováno při použití SQLite se strategií TPC. Generování na straně klienta nebo globálně jedinečné klíče , jako jsou identifikátory GUID, jsou však podporovány v jakékoli databázi, včetně SQLite.

Omezení cizího klíče

Strategie mapování TPC vytvoří denormalizované schéma SQL – to je jeden důvod, proč jsou některé databáze purists proti němu. Představte si například sloupec FavoriteAnimalIdcizího klíče . Hodnota v tomto sloupci musí odpovídat hodnotě primárního klíče některého zvířete. To lze vynutit v databázi s jednoduchým omezením FK při použití TPH nebo TPT. Příklad:

CONSTRAINT [FK_Animals_Animals_FavoriteAnimalId] FOREIGN KEY ([FavoriteAnimalId]) REFERENCES [Animals] ([Id])

Při použití TPC je však primární klíč pro každé dané zvíře uložen v tabulce odpovídající konkrétnímu typu daného zvířete. Primární klíč kočky je například uložen ve Cats.Id sloupci, zatímco primární klíč psa je uložen ve Dogs.Id sloupci atd. To znamená, že pro tuto relaci nelze vytvořit omezení FK.

V praxi to není problém, pokud se aplikace nepokoušá vložit neplatná data. Pokud jsou například všechna data vložena pomocí EF Core a používá navigace k propojení entit, je zaručeno, že sloupec FK bude obsahovat platné hodnoty PK vždy.

Shrnutí a pokyny

Stručně řečeno, TPH je obvykle v pořádku pro většinu aplikací a je dobrým výchozím nastavením pro širokou škálu scénářů, takže pokud ho nepotřebujete, nepřidávejte složitost TPC. Konkrétně platí, že pokud se váš kód bude dotazovat na entity mnoha typů, jako je zápis dotazů na základní typ, pak se naklonit k TPH přes TPC.

To znamená, že TPC je také dobrou strategií mapování, která se má použít, když se váš kód bude dotazovat na entity jednoho typu typu list a vaše srovnávací testy ukazují zlepšení v porovnání s TPH.

TpT používejte jenom v případě, že je to omezené externími faktory.