Megosztás:


Örökség

Az EF megfeleltethet egy .NET-típusú hierarchiát egy adatbázisnak. Így a .NET-entitásokat a szokásos módon, alap- és származtatott típusok használatával írhatja kódba, és zökkenőmentesen létrehozhatja a megfelelő adatbázissémát, lekérdezéseket adhat ki stb. A típushierarchia leképezésének tényleges részletei szolgáltatótól függenek; ez a lap egy relációs adatbázis kontextusában ismerteti az öröklés támogatását.

Entitástípus-hierarchialeképezés

Konvenció szerint az EF nem keres automatikusan alap- vagy származtatott típusokat; Ez azt jelenti, hogy ha azt szeretné, hogy a hierarchiában egy CLR-típus legyen leképezve, explicit módon meg kell adnia ezt a típust a modellen. Ha például csak a hierarchia alaptípusát adja meg, akkor az EF Core nem fogja implicit módon belefoglalni az összes altípusát.

Az alábbi minta egy DbSet-et nyújt Blog-nak és annak alosztályának, RssBlog-nek. Ha Blog rendelkezik más alosztálylal, az nem lesz belefoglalva a modellbe.

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

Megjegyzés

A TPH-megfeleltetés használatakor az adatbázisoszlopok szükség szerint automatikusan null értékűek lesznek. A RssUrl oszlop például null értékű, mert a normál Blog példányok nem rendelkeznek ezzel a tulajdonságpal.

Ha nem szeretne DbSet elérhetővé tenni egy vagy több entitáshoz a hierarchiában, a Fluent API-val is gondoskodhat arról, hogy azok szerepeljenek a modellben.

Tipp

Ha nem támaszkodik konvenciókra, az alaptípust explicit módon is megadhatja a HasBaseTypehasználatával. Az .HasBaseType((Type)null) használatával is eltávolíthat egy entitástípust a hierarchiából.

Hierarchiánkénti táblázat és diszkriminatív konfiguráció

Az EF alapértelmezés szerint a tábla/hierarchia (TPH) mintával képezi le az öröklődést. A TPH egyetlen táblával tárolja a hierarchiában lévő összes típus adatait, és egy diszkriminatív oszlop segítségével azonosítja, hogy az egyes sorok melyik típust jelölik.

A fenti modell a következő adatbázisséma szerint van leképezve (jegyezze fel az implicit módon létrehozott Discriminator oszlopot, amely azonosítja, hogy az egyes sorokban milyen típusú Blog van tárolva).

Képernyőkép a Blog entitáshierarchiájának lekérdezéséről a hierarchiánkénti táblamintával

Konfigurálhatja a diszkriminatív oszlop nevét és típusát, valamint a hierarchia egyes típusainak azonosításához használt értékeket:

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

A fenti példákban az EF implicit módon hozzáadta a diszkriminátort árnyéktulajdonságként a hierarchia alapentitásán. Ez a tulajdonság bármely máshoz hasonlóan konfigurálható:

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

Végül a diszkriminátor egy normál .NET-tulajdonságra is leképezhető az entitásban.

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

A TPH-mintát használó származtatott entitások lekérdezésekor az EF Core egy predikátumot ad hozzá a diszkriminatív oszlophoz a lekérdezésben. Ez a szűrő gondoskodik arról, hogy ne kapjunk további sorokat az eredményben nem szereplő alap- vagy testvértípusokhoz. A szűrő predikátumát a rendszer kihagyja az alapentitástípushoz, mivel az alapentitással kapcsolatos lekérdezések a hierarchia összes entitására vonatkozóan kapnak eredményt. Ha egy lekérdezés eredményeinek materializálásakor olyan diszkriminatív értékkel találkozunk, amely nem felel meg a modell egyik entitástípusának sem, kivételt teszünk, mivel nem tudjuk, hogyan kell az eredményeket materializálni. Ez a hiba csak akkor fordul elő, ha az adatbázis diszkriminatív értékeket tartalmazó sorokat tartalmaz, amelyek nincsenek leképezve az EF-modellben. Ha ilyen adatokkal rendelkezik, akkor az EF Core-modell diszkriminatív leképezését hiányosként jelölheti meg, hogy jelezze, hogy a hierarchia bármely típusának lekérdezéséhez mindig hozzá kell adnunk szűrő predikátumot. IsComplete(false) diszkriminatív konfiguráció meghívása a leképezést hiányosnak jelöli.

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

Megosztott oszlopok

Alapértelmezés szerint, ha a hierarchia két testvér entitástípusa azonos nevű tulajdonsággal rendelkezik, a rendszer két különálló oszlopra rendeli őket. Ha azonban a típusuk megegyezik, azokat ugyanarra az adatbázisoszlopra lehet leképezni:

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

Megjegyzés

A relációsadatbázis-szolgáltatók, például az SQL Server, nem használják automatikusan a diszkriminatív predikátumot a megosztott oszlopok lekérdezésekor, amikor leadást használnak. A Url = (blog as RssBlog).Url lekérdezés a testvér Url sorainak Blog értékét is visszaadja. Ha a lekérdezést RssBlog entitásokra szeretné korlátozni, manuálisan kell szűrőt hozzáadnia a diszkriminatívhoz, például Url = blog is RssBlog ? (blog as RssBlog).Url : null.

Táblatípusonkénti konfiguráció

A TPT-leképezési mintában az összes típus az egyes táblákhoz van megfeleltetve. A kizárólag alaptípushoz vagy származtatott típushoz tartozó tulajdonságok egy olyan táblában vannak tárolva, amely megfelel az adott típusnak. A származtatott típusok leképezésére szolgáló táblák egy idegen kulcsot is tárolnak, amely a származtatott táblát az alaptáblával összekapcsolja.

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

Tipp

Ahelyett, hogy ToTable hívna meg minden entitástípusnál, meghívhatja modelBuilder.Entity<Blog>().UseTptMappingStrategy() minden egyes fő entitástípusnál, és az EF generálja a táblaneveket.

Tipp

Az egyes táblák elsődleges kulcsoszlopainak különböző neveinek beállításához lásd a tábla-specifikus jellemzőkonfigurációkat.

Az EF a fenti modellhez a következő adatbázissémát hozza létre.

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

Megjegyzés

Ha az elsődleges kulcs kényszerét átnevezik, az új nevet a rendszer a hierarchiára leképezett összes táblára alkalmazza, a jövőbeli EF-verziók csak egy adott táblára engedélyezik a korlátozás átnevezését, ha az 19970-es probléma ki lett javítva.

Ha tömeges konfigurációt használ, lekérheti egy adott tábla oszlopnevét a GetColumnName(IProperty, StoreObjectIdentifier)meghívásával.

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

Figyelmeztetés

A TPT sok esetben alacsonyabb teljesítményt mutat a TPH-hez képest. További információért tekintse meg a teljesítmény-dokumentációt.

Figyelmeztetés

A származtatott típus oszlopai különböző táblákra vannak leképezve, ezért az örökölt és deklarált tulajdonságokat egyaránt használó összetett FK-megkötések és indexek nem hozhatók létre az adatbázisban.

Tábla/beton típusú konfiguráció

A TPC-leképezési mintában az összes típus egyedi táblákhoz van megfeleltetve. Minden tábla a megfelelő entitástípus összes tulajdonságához tartalmaz oszlopokat. Ez a TPT-stratégia néhány gyakori teljesítményproblémájával foglalkozik.

Tipp

Az EF-csapat részletesen bemutatta és részletesen beszélt a TPC-leképezésről a .NET Data Community Standupegy epizódjában. Mint minden közösségi standup epizód, akkor nézni a TPC epizód most a YouTube.

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

Tipp

Ahelyett, hogy minden egyes entitástípuson végrehajtanánk a ToTable hívását, elegendő a modelBuilder.Entity<Blog>().UseTpcMappingStrategy() hívása minden egyes gyökérentitástípuson, hogy konvenciók alapján létrejöjjenek a táblanevek.

Tipp

Az egyes táblák elsődleges kulcsoszlopainak különböző neveinek beállításához lásd a tábla-specifikus jellemzőkonfigurációkat.

Az EF a fenti modellhez a következő adatbázissémát hozza létre.

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

TPC-adatbázisséma

A TPC-stratégia hasonló a TPT-stratégiához, kivéve, hogy a hierarchiában minden konkrét típushoz más tábla jön létre, de a táblák nem jönnek létre absztrakt típusokhoz – innen a "table-per-concrete-type" elnevezés. A TPT-hez hasonlóan maga a tábla is jelzi a mentett objektum típusát. A TPT-megfeleltetéstől eltérően azonban minden tábla oszlopokat tartalmaz a konkrét típus minden tulajdonságához és alaptípusához. A TPC-adatbázis sémái denormalizáltak.

Fontolja meg például a hierarchia leképezését:

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

SQL Server használata esetén a hierarchia számára létrehozott táblák a következők:

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

Figyelje meg az alábbiakat:

  • Nincsenek táblák a Animal vagy Pet típusokhoz, mivel ezek abstract az objektummodellben. Ne feledje, hogy a C# nem engedélyezi az absztrakt típusú példányokat, ezért nincs olyan helyzet, amikor egy absztrakt típusú példányt mentenének az adatbázisba.

  • Az alaptípusok tulajdonságainak leképezése minden egyes betontípus esetében ismétlődik. Például minden táblának van egy Name oszlopa, és a Macskák és a Kutyák esetében van egy Vet oszlop.

  • Az adatok adatbázisba való mentése a következőket eredményezi:

Macskák táblázat

Azonosító Név FoodId Állatorvos Oktatási szint
1 Alice 99ca3e98-b26d-4a0c-d4ae-08da7aca624f Pengelly MBA (Üzleti Tanulmányok Mestere)
2 Mac 99ca3e98-b26d-4a0c-d4ae-08da7aca624f Pengelly Óvoda
8 Baxter 5dc5019e-6f72-454b-d4b0-08da7aca624f Bothell Pet Hospital Bsc

Kutyák táblázat

Azonosító Név FoodId Állatorvos FavoriteToy
3 Pirítós 011aaf6f-d588-4fad-d4ac-08da7aca624f Pengelly Mókus úr

Háziállatok Tábla

Azonosító Név FoodId Érték Faj
4 Clyde 1d495075-f527-4498-d4af-08da7aca624f 100,00 Equus africanus asinus (házi szamár)

Emberek tábla

Azonosító Név FoodId KedvencÁllatAzonosító
5 Wendy 5418fd81-7660-432f-d4b1-08da7aca624f 2
6 Arthur 59b495d4-0414-46bf-d4ad-08da7aca624f 1
9 Katie nulla 8

Figyelje meg, hogy a TPT-leképezéstől eltérően egyetlen objektum összes információja egyetlen táblában található. A TPH-megfeleltetéssel ellentétben egyetlen olyan táblában sem szerepel oszlop és sor kombináció, amelyben a modell soha nem használja. Az alábbiakban bemutatjuk, hogyan lehetnek fontosak ezek a jellemzők a lekérdezések és a tárolás szempontjából.

Kulcsgenerálás

A választott öröklés-leképezési stratégia következményekkel jár az elsődleges kulcsértékek létrehozásának és kezelésének módjára. A TPH-kulcsok egyszerűek, mivel minden entitáspéldányt egyetlen sor jelöl egyetlen táblában. Bármilyen kulcsérték-létrehozás használható, és nincs szükség további korlátozásokra.

A TPT-stratégia esetében mindig van egy sor a táblában, amely a hierarchia alaptípusára van megfeleltetve. Ezen a sorban bármilyen kulcslétrehozás használható, és a többi tábla kulcsai idegen kulcskorlátozások használatával vannak összekapcsolva ehhez a táblához.

A dolgok egy kicsit bonyolultabbak lesznek a TPC számára. Először is fontos tisztában lenni azzal, hogy az EF Core megköveteli, hogy a hierarchia összes entitása egyedi kulcsértékekkel rendelkezzen, még akkor is, ha az entitások különböző típusúak. A példamodell használatával például egy kutya nem rendelkezhet ugyanazzal az azonosítókulcs-értékkel, mint a Macska. Másodszor, a TPT-sel ellentétben nincs olyan közös táblázat, amely egyetlen helyként működhet, ahol a kulcsértékek élnek, és létrehozhatóak. Ez azt jelenti, hogy egy egyszerű Identity oszlop nem használható.

A sorozatokat támogató adatbázisok esetében a kulcsértékek az egyes táblák alapértelmezett korlátozásában hivatkozott egyetlen sorozat használatával hozhatók létre. Ez a fenti TPC-táblákban használt stratégia, amelyben minden tábla a következő:

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

AnimalSequence az EF Core által létrehozott adatbázis-sorozat. Ez a stratégia alapértelmezés szerint tPC-hierarchiákhoz használatos az SQL Server EF Core-adatbázis-szolgáltatójának használatakor. A sorozatokat támogató más adatbázisok adatbázis-szolgáltatóinak hasonló alapértelmezett értékekkel kell rendelkezniük. A TPC-vel más kulcsgenerálási stratégiák is használhatók, amelyek szekvenciákat, például Hi-Lo-mintákat használnak.

Bár a standard identitásoszlopok nem működnek a TPC-vel, az identitásoszlopok akkor használhatók, ha minden tábla megfelelő maggal és növekménylel van konfigurálva, így az egyes táblákhoz létrehozott értékek soha nem ütköznek. Például:

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

Fontos

Ezzel a stratégiával később nehezebb lesz származtatott típusokat hozzáadni, mivel a hierarchiában lévő típusok teljes számának előzetes ismerete szükséges.

Az SQLite nem támogatja a szekvenciákat vagy az identitásmag/növekményeket, ezért az egész szám kulcsértékének létrehozása nem támogatott az SQLite TPC-stratégiával való használatakor. Az ügyféloldali generáló vagy globálisan egyedi kulcsok – például a GRAFIKUS AZONOSÍTÓk – azonban bármely adatbázisban támogatottak, beleértve az SQLite-et is.

Idegenkulcs-korlátozások

A TPC-leképezési stratégia denormalizált SQL-sémát hoz létre – ez az egyik oka annak, hogy egyes adatbázis-puristák ellene vannak. Vegyük például az idegenkulcsos oszlopot FavoriteAnimalId. Az oszlopban szereplő értéknek meg kell egyeznie egy állat elsődleges kulcsértékével. Ez a TPH vagy TPT használatakor egyszerű FK-korlátozással kényszeríthető ki az adatbázisban. Például:

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

A TPC használatakor azonban az adott állat elsődleges kulcsát az adott állat konkrét típusának megfelelő táblázatban tárolják. A macska elsődleges kulcsát például a Cats.Id oszlop tárolja, míg a kutya elsődleges kulcsát a Dogs.Id oszlopban stb. Ez azt jelenti, hogy ehhez a kapcsolathoz nem hozható létre FK-korlátozás.

Ez a gyakorlatban nem jelent problémát, ha az alkalmazás nem próbál érvénytelen adatokat beszúrni. Ha például az EF Core beszúrja az összes adatot, és navigációkkal kapcsol össze entitásokat, akkor garantált, hogy az FK oszlop mindig érvényes PK-értékeket fog tartalmazni.

Összefoglalás és útmutató

Összefoglalva, a TPH általában a legtöbb alkalmazás esetében rendben van, és számos forgatókönyv esetében jó alapértelmezett, ezért ne adja hozzá a TPC összetettségét, ha nincs rá szüksége. Pontosabban, ha a kód többnyire számos típusú entitást kérdez le, például lekérdezéseket ír az alaptípusra, akkor a TPH felé hajol a TPC-n keresztül.

Ennek ellenére a TPC is jó leképezési stratégia, ha a kód többnyire egyetlen levéltípusú entitásokat kérdez le, és a teljesítménytesztek javulást mutatnak a TPH-hoz képest.

Csak akkor használja a TPT-t, ha ezt külső tényezők korlátozzák.