Delen via


Erfenis

EF kan een .NET-typehiërarchie toewijzen aan een database. Hierdoor kunt u uw .NET-entiteiten zoals gebruikelijk in code schrijven, gebruikmaken van basis- en afgeleide typen en EF naadloos het juiste databaseschema maken, query's uitvoeren, enzovoort. De werkelijke details van hoe een type-hiërarchie wordt toegewezen, zijn afhankelijk van de provider; op deze pagina wordt ondersteuning voor overerving beschreven in de context van een relationele database.

Hiërarchietoewijzing van entiteitstype

Over het algemeen scant EF niet automatisch op basis- of afgeleide typen; dit betekent dat als u wilt dat een CLR-type in uw hiërarchie wordt toegewezen, u dat type expliciet opgeeft in uw model. Als u bijvoorbeeld alleen het basistype van een hiërarchie opgeeft, zorgt dat er niet voor dat EF Core al zijn subtypen impliciet opneemt.

In het volgende voorbeeld wordt een DbSet voor Blog en de bijbehorende subklasse RssBlogweergegeven. Als Blog er een andere subklasse is, wordt deze niet opgenomen in het model.

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

Notitie

Databasekolommen kunnen indien nodig automatisch null-waarden bevatten bij het gebruik van TPH-toewijzing. De RssUrl kolom is bijvoorbeeld null-baar omdat gewone Blog exemplaren die eigenschap niet hebben.

Als u een DbSet voor een of meer entiteiten in de hiërarchie niet beschikbaar wilt maken, kunt u ook de Fluent-API gebruiken om ervoor te zorgen dat ze zijn opgenomen in het model.

Hint

Als u niet afhankelijk bent van conventies, kunt u het basistype expliciet opgeven met behulp van HasBaseType. U kunt ook .HasBaseType((Type)null) een entiteitstype uit de hiërarchie verwijderen.

Configuratie van tabel per hiërarchie en discriminator

Standaard mapt EF de erfenisstructuur met behulp van het table-per-hierarchy (TPH) patroon. TPH maakt gebruik van één tabel voor het opslaan van de gegevens voor alle typen in de hiërarchie en er wordt een discriminatorkolom gebruikt om te bepalen welk type elke rij vertegenwoordigt.

Het bovenstaande model wordt toegewezen aan het volgende databaseschema (let op de impliciet gemaakte Discriminator kolom, waarmee wordt aangegeven welk type Blog wordt opgeslagen in elke rij).

Schermopname van de resultaten van het uitvoeren van query's op de hiërarchie van de blogentiteit met behulp van een tabel-per-hiërarchiepatroon

U kunt de naam en het type van de discriminatorkolom en de waarden configureren die worden gebruikt om elk type in de hiërarchie te identificeren:

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

In de bovenstaande voorbeelden heeft EF impliciet de discriminator toegevoegd als een schaduweigenschap op de basisentiteit van de hiërarchie. Deze eigenschap kan net als elke andere worden geconfigureerd:

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

Ten slotte kan de discriminator ook worden toegewezen aan een reguliere .NET-eigenschap in uw entiteit:

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

Bij het uitvoeren van query's op afgeleide entiteiten, die gebruikmaken van het TPH-patroon, voegt EF Core een predicaat toe boven de discriminatorkolom in de query. Dit filter zorgt ervoor dat er geen extra rijen worden opgehaald voor basistypen of verwante typen die niet in het resultaat voorkomen. Dit filterpredicaat wordt overgeslagen voor het type basisentiteit, omdat het opvragen voor de basisentiteit resultaten krijgt voor alle entiteiten in de hiërarchie. Bij het materialiseren van resultaten van een query, als we een discriminatorwaarde tegenkomen, die niet is toegewezen aan een entiteitstype in het model, genereren we een uitzondering omdat we niet weten hoe we de resultaten moeten materialiseren. Deze fout treedt alleen op als uw database rijen bevat met discriminatorwaarden die niet in het EF-model zijn gemapt. Als u dergelijke gegevens hebt, kunt u de discriminatortoewijzing in het EF Core-model markeren als onvolledig om aan te geven dat we altijd filterpredicaat moeten toevoegen voor het uitvoeren van query's op elk type in de hiërarchie. IsComplete(false) een oproep aan de discriminatorconfiguratie markeert dat de mapping onvolledig is.

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

Gedeelde kolommen

Wanneer twee entiteitstypen op hetzelfde niveau in de hiërarchie een eigenschap met dezelfde naam hebben, worden deze standaard toegewezen aan twee afzonderlijke kolommen. Als hun type echter identiek is, kunnen ze worden toegewezen aan dezelfde databasekolom:

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

Notitie

Relationele databaseproviders, zoals SQL Server, gebruiken niet automatisch het discriminatiepredicaat bij het uitvoeren van query's op gedeelde kolommen wanneer u een cast gebruikt. De query Url = (blog as RssBlog).Url retourneert ook de Url waarde voor de gerelateerde rijen Blog. Als u de query wilt beperken tot RssBlog entiteiten, moet u handmatig een filter toevoegen aan de discriminator, zoals Url = blog is RssBlog ? (blog as RssBlog).Url : null.

Configuratie van tabel per type

In het TPT-toewijzingspatroon worden alle typen toegewezen aan afzonderlijke tabellen. Eigenschappen die uitsluitend tot een basistype of afgeleid type behoren, worden opgeslagen in een tabel die aan dat type is toegewezen. Tabellen die zijn toegewezen aan afgeleide typen slaan ook een refererende sleutel op waarmee de afgeleide tabel wordt samengevoegd met de basistabel.

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

Hint

In plaats van ToTable voor elk entiteitstype aan te roepen, kunt u modelBuilder.Entity<Blog>().UseTptMappingStrategy() aanroepen voor elk hoofdentiteitstype en worden de tabelnamen gegenereerd door EF.

Hint

Zie tabelspecifieke facetconfiguratie als u verschillende kolomnamen wilt configureren voor de primaire-sleutelkolommen in elke tabel.

EF maakt het volgende databaseschema voor het bovenstaande 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
);

Notitie

Als de naam van de primaire-sleutelbeperking wordt gewijzigd, wordt de nieuwe naam toegepast op alle tabellen die aan de hiërarchie zijn toegewezen, kunnen toekomstige EF-versies de naam van de beperking alleen voor een bepaalde tabel wijzigen wanneer probleem 19970 is opgelost.

Als u bulkconfiguratie gebruikt, kunt u de kolomnaam voor een specifieke tabel ophalen door aan te roepen 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();
}

Waarschuwing

In veel gevallen vertoont TPT minder goede prestaties in vergelijking met TPH. Zie de prestatiedocumenten voor meer informatie.

Waarschuwing

Kolommen voor een afgeleid type worden toegewezen aan verschillende tabellen. Samengestelde FK-beperkingen en -indexen die zowel de overgenomen als gedeclareerde eigenschappen gebruiken, kunnen niet worden gemaakt in de database.

Configuratie van een tabel per specifiek type

In het TPC-toewijzingspatroon worden alle typen toegewezen aan afzonderlijke tabellen. Elke tabel bevat kolommen voor alle eigenschappen van het bijbehorende entiteitstype. Hiermee worden enkele veelvoorkomende prestatieproblemen met de TPT-strategie opgelost.

Hint

Het EF-team demonstreerde en sprak uitgebreid over TPC-mapping in een aflevering van de .NET Data Community Standup. Net als bij alle Community Standup-afleveringen kun je de TPC-aflevering nu bekijken op YouTube.

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

Hint

In plaats van elk entiteitstype aan te roepen ToTable , worden de tabelnamen standaard gegenereerd door elk type hoofdentiteit aan te roepen modelBuilder.Entity<Blog>().UseTpcMappingStrategy() .

Hint

Zie tabelspecifieke facetconfiguratie als u verschillende kolomnamen wilt configureren voor de primaire-sleutelkolommen in elke tabel.

EF maakt het volgende databaseschema voor het bovenstaande 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])
);

TPC-databaseschema

De TPC-strategie is vergelijkbaar met de TPT-strategie, behalve dat er een andere tabel wordt gemaakt voor elk concreet type in de hiërarchie, maar tabellen worden niet gemaakt voor abstracte typen, vandaar de naam 'tabel-per-beton-type'. Net als bij TPT geeft de tabel zelf het type van het object aan dat is opgeslagen. In tegenstelling tot TPT-toewijzing bevat elke tabel echter kolommen voor elke eigenschap in het betontype en de basistypen. TPC-databaseschema's worden gedenormaliseerd.

Denk bijvoorbeeld aan het in kaart brengen van deze hiërarchie:

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

Wanneer u SQL Server gebruikt, zijn de tabellen die voor deze hiërarchie zijn gemaakt:

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

U ziet dat:

  • Er zijn geen tabellen voor de Animal of Pet typen, omdat deze zich abstract in het objectmodel bevinden. Houd er rekening mee dat C# geen abstracte typen toestaat, en er is dus geen situatie waarin een abstract type-exemplaar wordt opgeslagen in de database.

  • De toewijzing van eigenschappen in basistypen wordt herhaald voor elk concreet type. Elke tabel heeft bijvoorbeeld een Name kolom en katten en honden hebben een Vet kolom.

  • Het opslaan van enkele gegevens in deze database resulteert in het volgende:

Kattentabel

Identiteitsbewijs Naam FoodId Dierenarts Onderwijsniveau
1 Alice 99ca3e98-b26d-4a0c-d4ae-08da7aca624f Pengelly Master of Business Administration
2 Mac 99ca3e98-b26d-4a0c-d4ae-08da7aca624f Pengelly Preschool
8 Baxter 5dc5019e-6f72-454b-d4b0-08da7aca624f Bothell Pet Hospital Bsc

Hondentafel

Identiteitsbewijs Naam FoodId Dierenarts FavoriteToy
3 Geroosterd brood 011aaf6f-d588-4fad-d4ac-08da7aca624f Pengelly Meneer Eekhoorn

Tabel FarmAnimals

Identiteitsbewijs Naam FoodId Waarde Soort
4 Clyde 1d495075-f527-4498-d4af-08da7aca624f 100.00 Equus africanus asinus

Tabel Mensen

Identiteitsbewijs Naam FoodId FavorieteDierId
5 Wendy 5418fd81-7660-432f-d4b1-08da7aca624f 2
6 Arthur 59b495d4-0414-46bf-d4ad-08da7aca624f 1
9 Katie Nul 8

Merk op dat in tegenstelling tot TPT-toewijzing alle informatie voor één object zich in één tabel bevindt. En, in tegenstelling tot TPH-toewijzing, is er geen combinatie van kolom en rij in een tabel waar dat nooit door het model wordt gebruikt. Hieronder ziet u hoe deze kenmerken belangrijk kunnen zijn voor query's en opslag.

Sleutelgeneratie

De gekozen strategie voor overerving heeft gevolgen voor de wijze waarop primaire sleutelwaarden worden gegenereerd en beheerd. Sleutels in TPH zijn eenvoudig, omdat elk entiteitsexemplaar wordt vertegenwoordigd door één rij in één tabel. Elk type sleutelwaarde genereren kan worden gebruikt en er zijn geen extra beperkingen nodig.

Voor de TPT-strategie is er altijd een rij in de tabel die is toegewezen aan het basistype van de hiërarchie. Elke vorm van sleutelgeneratie kan op deze rij worden toegepast, en de sleutels voor andere tabellen worden aan deze tabel gekoppeld met behulp van foreign key-beperkingen.

Dingen worden wat ingewikkelder voor TPC. Ten eerste is het belangrijk om te begrijpen dat EF Core vereist dat alle entiteiten in een hiërarchie een unieke sleutelwaarde hebben, zelfs als de entiteiten verschillende typen hebben. Als u bijvoorbeeld ons voorbeeldmodel gebruikt, kan een hond niet dezelfde id-sleutelwaarde hebben als een kat. Ten tweede, in tegenstelling tot TPT, is er geen gemeenschappelijke tabel die kan fungeren als de enige plaats waar sleutelwaarden zich bevinden en kunnen worden gegenereerd. Dit betekent dat een eenvoudige Identity kolom niet kan worden gebruikt.

Voor databases die reeksen ondersteunen, kunnen sleutelwaarden worden gegenereerd met behulp van één reeks waarnaar wordt verwezen in de standaardbeperking voor elke tabel. Dit is de strategie die wordt gebruikt in de bovenstaande TPC-tabellen, waarbij elke tabel het volgende heeft:

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

AnimalSequence is een databasereeks die is gemaakt door EF Core. Deze strategie wordt standaard gebruikt voor TPC-hiërarchieën bij het gebruik van de EF Core-databaseprovider voor SQL Server. Databaseproviders voor andere databases die reeksen ondersteunen, moeten een vergelijkbare standaardwaarde hebben. Andere strategieën voor het genereren van sleutels die gebruikmaken van reeksen, zoals Hi-Lo-patronen, kunnen ook worden gebruikt met TPC.

Hoewel standaardidentiteitskolommen niet werken met TPC, is het mogelijk om identiteitskolommen te gebruiken als elke tabel is geconfigureerd met een geschikte seed en increment, zodat de waarden die voor elke tabel worden gegenereerd, nooit conflicteren. Voorbeeld:

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

Belangrijk

Als u deze strategie gebruikt, is het later moeilijker om afgeleide typen toe te voegen, omdat hiervoor het totale aantal typen in de hiërarchie vooraf bekend moet zijn.

SQLite biedt geen ondersteuning voor reeksen of identity seed/increment, en daarom wordt het genereren van gehele sleutelwaarden niet ondersteund bij het gebruik van SQLite met de TPC-strategie. Het genereren van clientzijde of wereldwijd unieke sleutels, zoals GUID's, worden echter ondersteund in elke database, waaronder SQLite.

Beperkingen van referentiesleutels

Met de TPC-toewijzingsstrategie wordt een gedenormaliseerd SQL-schema gemaakt. Dit is een van de redenen waarom sommige database-puristen er tegen zijn. Denk bijvoorbeeld aan de kolom FavoriteAnimalIdmet refererende sleutels. De waarde in deze kolom moet overeenkomen met de primaire sleutelwaarde van een bepaald dier. Dit kan worden afgedwongen in de database met een eenvoudige FK-beperking wanneer u TPH of TPT gebruikt. Voorbeeld:

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

Maar bij het gebruik van TPC wordt de primaire sleutel voor een bepaald dier opgeslagen in de tabel die overeenkomt met het concrete type van dat dier. De primaire sleutel van een kat wordt bijvoorbeeld opgeslagen in de Cats.Id kolom, terwijl de primaire sleutel van een hond wordt opgeslagen in de Dogs.Id kolom, enzovoort. Dit betekent dat er geen FK-beperking kan worden gemaakt voor deze relatie.

In de praktijk is dit geen probleem zolang de toepassing geen ongeldige gegevens probeert in te voegen. Als bijvoorbeeld alle gegevens worden ingevoegd door EF Core en navigatie gebruikt om entiteiten te relateren, wordt gegarandeerd dat de FK-kolom altijd geldige PK-waarden bevat.

Samenvatting en richtlijnen

Kortom, TPH is meestal prima voor de meeste toepassingen en is een goede standaardinstelling voor een breed scala aan scenario's, dus voeg de complexiteit van TPC niet toe als u deze niet nodig hebt. Als uw code voornamelijk query's uitvoert op entiteiten van veel typen, zoals het schrijven van query's op basis van het basistype, leunt u dan naar TPH via TPC.

Dat gezegd hebbende, TPC is ook een goede toewijzingsstrategie die moet worden gebruikt wanneer uw code voornamelijk query's uitvoert op entiteiten van één bladtype en uw benchmarks een verbetering laten zien in vergelijking met TPH.

Gebruik TPT alleen als dit wordt beperkt door externe factoren.