Sdílet prostřednictvím


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 RssUrl je nullable, protože běžné instance Blog tuto vlastnost nemají.

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

Návod

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 diskriminátoru

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 sloupce Discriminator, který identifikuje, jaký typ Blog je uložen v každém řádku).

Snímek obrazovky s výsledky dotazování na hierarchii entit blogu pomocí vzoru table-per-hierarchy

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("blog_type")
        .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í vzorec TPH, EF Core přidá podmínku přes diskriminační sloupec v dotazu. Tento filtr zajistí, že ve výsledku nezískáme žádné další řádky pro základní typy nebo příbuzné typy. 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) vyžádat si konfiguraci diskriminátoru 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í, pokud dva sourozenecké typy entit 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 diskriminační predikát. Dotaz Url = (blog as RssBlog).Url by také vrátil Url hodnotu pro sousední Blog řádky. 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");

Návod

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

Návod

Pokud chcete nakonfigurovat různé názvy sloupců primárního klíče v každé tabulce, podívejte se na konfiguraci aspektu specifickou 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();
}

Výstraha

V mnoha případech TPT vykazuje 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 tabulky pro konkrétní typ

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.

Návod

Tým EF předvedl a podrobně diskutoval o mapování TPC v epizodě Standupu komunity .NET Data. 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");

Návod

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.

Návod

Pokud chcete nakonfigurovat různé názvy sloupců primárního klíče v každé tabulce, podívejte se na konfiguraci aspektu specifickou 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-concrete-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 typy Animal nebo 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. Například, každá tabulka má sloupec Name a kočky i psi mají také sloupec Vet.

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

Tabulka koček

identifikátor Název FoodId Veterinář Úroveň vzdělání
1 Eliška 99ca3e98-b26d-4a0c-d4ae-08da7aca624f Pengelly MBA
2 Macintosh 99ca3e98-b26d-4a0c-d4ae-08da7aca624f Pengelly Předškolní
8 Baxter 5dc5019e-6f72-454b-d4b0-08da7aca624f Bothell Pet Hospital Bsc

Psí tabulka

identifikátor Název FoodId Veterinář FavoriteToy
3 Zapečený chléb 011aaf6f-d588-4fad-d4ac-08da7aca624f Pengelly Pan Veverka

Tabulka Hospodářská Zvířata

identifikátor Název FoodId Hodnota Druh
4 Clyde 1d495075-f527-4498-d4af-08da7aca624f 100.00 Equus africanus asinus (osel domácí)

Tabulka lidí

identifikátor Název FoodId IDOblíbenéhoZvířete
5 Wendy 5418fd81-7660-432f-d4b1-08da7aca624f 2
6 Artuš 59b495d4-0414-46bf-d4ad-08da7aca624f 1
9 Katie nula 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, kterou model nikdy nepoužije. 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 databázová sekvence 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. Napří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ěkteří databázoví puristé proti ní. Představte si například sloupec cizího klíče FavoriteAnimalId. Hodnota v tomto sloupci musí odpovídat hodnotě primárního klíče nějakého zvířete. To lze vynutit v databázi s jednoduchým omezením FK při použití TPH nebo TPT. Napří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 různých typů, jako je zadávání dotazů na základní typy, pak preferujte TPH před TPC.

To znamená, že TPC je také dobrou strategií mapování, kterou lze použít, když váš kód bude většinou dotazovat na entity jednoho konkrétního typu listu 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.