Anteckning
Åtkomst till den här sidan kräver auktorisering. Du kan prova att logga in eller ändra kataloger.
Åtkomst till den här sidan kräver auktorisering. Du kan prova att ändra kataloger.
EF kan mappa en .NET-typhierarki till en databas. På så sätt kan du skriva dina .NET-entiteter i kod som vanligt med hjälp av bas- och härledda typer och låta EF sömlöst skapa rätt databasschema, utfärda frågor osv. Den faktiska informationen om hur en typhierarki mappas är leverantörsberoende. På den här sidan beskrivs arvsstöd i kontexten för en relationsdatabas.
Mappning av entitetstyphierarki
Enligt konventionen söker EF inte automatiskt efter bastyper eller härledda typer. Det innebär att om du vill att en CLR-typ i hierarkin ska mappas måste du uttryckligen ange den typen i din modell. Om du till exempel bara anger bastypen för en hierarki kommer EF Core inte implicit att inkludera alla dess undertyper.
Följande exempel visar en DbSet för Blog och dess underklass RssBlog. Om Blog har någon annan underklass tas den inte med i modellen.
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; }
}
Anmärkning
Databaskolumner blir automatiskt nullbara vid behov när du använder TPH-mappning. Till exempel kan kolumnen RssUrl vara null eftersom vanliga Blog-instanser inte har den egenskapen.
Om du inte vill exponera en DbSet för en eller flera entiteter i hierarkin kan du också använda Fluent API för att säkerställa att de ingår i modellen.
Tips
Om du inte förlitar dig på konventionerkan du ange bastypen explicit med hjälp av HasBaseType. Du kan också använda .HasBaseType((Type)null) för att ta bort en entitetstyp från hierarkin.
Tabell-per-hierarki och diskriminerande konfiguration
Som standard mappar EF arvet med hjälp av TPH-mönster (table-per-hierarchy). TPH använder en enskild tabell för att lagra data för alla typer i hierarkin, och en diskriminerande kolumn används för att identifiera vilken typ varje rad representerar.
Modellen ovan mappas till följande databasschema (observera den implicit skapade kolumnen Discriminator, som identifierar vilken typ av Blog lagras på varje rad).
Du kan konfigurera namnet och typen av den diskriminerande kolumnen och de värden som används för att identifiera varje typ i hierarkin:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.HasDiscriminator<string>("blog_type")
.HasValue<Blog>("blog_base")
.HasValue<RssBlog>("blog_rss");
}
I exemplen ovan lade EF till diskriminatorn implicit som en skuggegenskap på hierarkins basentitet. Den här egenskapen kan konfigureras som alla andra:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.Property("blog_type")
.HasMaxLength(200);
}
Slutligen kan diskrimineringen också mappas till en vanlig .NET-egenskap i din entitet:
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>();
}
Vid frågor om härledda entiteter som använder TPH-mönstret, lägger EF Core till ett predikat över diskriminator-kolumnen i frågan. Det här filtret ser till att vi inte får några ytterligare rader för bastyper eller syskontyper som inte ingår i resultatet. Det här filterpredikatet hoppas över för basentitetstypen eftersom frågan för basentiteten får resultat för alla entiteter i hierarkin. När vi materialiserar resultat från en fråga, om vi stöter på ett diskriminerande värde, som inte är mappat till någon entitetstyp i modellen, utlöser vi ett undantag eftersom vi inte vet hur resultatet ska materialiseras. Det här felet uppstår bara om databasen innehåller rader med diskriminerande värden, som inte mappas i EF-modellen. Om du har sådana data kan du markera den diskriminerande mappningen i EF Core-modellen som ofullständig för att indikera att vi alltid bör lägga till filterpredikat för att fråga någon typ i hierarkin.
IsComplete(false) anrop på diskriminerarkonfigurationen markerar att mappningen är ofullständig.
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.HasDiscriminator()
.IsComplete(false);
}
Delade kolumner
När två samma entitetstyper i hierarkin har en egenskap med samma namn mappas de som standard till två separata kolumner. Men om deras typ är identisk kan de mappas till samma databaskolumn:
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; }
}
Anmärkning
Leverantörer av relationsdatabaser, till exempel SQL Server, använder inte automatiskt det diskriminerande predikatet när frågor körs mot delade kolumner med en cast. Förfrågan Url = (blog as RssBlog).Url skulle också returnera värdet Url för syskonraderna Blog. Om du vill begränsa sökfrågan till RssBlog-entiteter måste du manuellt lägga till ett filter för diskriminatorn, till exempel Url = blog is RssBlog ? (blog as RssBlog).Url : null.
Tabell-per-typ-konfiguration
I TPT-mappningsmönstret mappas alla typer till enskilda tabeller. Egenskaper som endast tillhör en bastyp eller härledd typ lagras i en tabell som mappar till den typen. Tabeller som mappar till härledda typer lagrar också en sekundärnyckel som kopplar den härledda tabellen till bastabellen.
modelBuilder.Entity<Blog>().ToTable("Blogs");
modelBuilder.Entity<RssBlog>().ToTable("RssBlogs");
Tips
Istället för att anropa ToTable för varje entitetstyp kan du anropa modelBuilder.Entity<Blog>().UseTptMappingStrategy() för varje rotentitetstyp och tabellnamnen genereras av EF.
Tips
Information om hur du konfigurerar olika kolumnnamn för primärnyckelkolumnerna i varje tabell finns i Tabellspecifik fasetteringskonfiguration.
EF skapar följande databasschema för modellen ovan.
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
);
Anmärkning
Om den primära nyckelbegränsningen byts namn tillämpas det nya namnet på alla tabeller som mappas till hierarkin. Framtida EF-versioner tillåter endast namnbyte av villkoret för en viss tabell när problem 19970 har åtgärdats.
Om du använder masskonfiguration kan du hämta kolumnnamnet för en viss tabell genom att anropa 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();
}
Varning
I många fall visar TPT sämre prestanda jämfört med TPH. Mer information finns i prestandadokumenten.
Försiktighet
Kolumner för en härledd typ mappas till olika tabeller, och därför går det inte att skapa sammansatta FK-begränsningar och index som använder både de ärvda och deklarerade egenskaperna i databasen.
Konfiguration av tabell per betongtyp
I TPC-mappningsmönstret mappas alla typer till enskilda tabeller. Varje tabell innehåller kolumner för alla egenskaper för motsvarande entitetstyp. Detta åtgärdar några vanliga prestandaproblem med TPT-strategin.
Tips
EF-teamet demonstrerade och talade ingående om TPC-mappning i ett avsnitt av .NET Data Community Standup. Precis som med alla Community Standup-avsnitt kan du titta på TPC-avsnittet nu på YouTube.
modelBuilder.Entity<Blog>().UseTpcMappingStrategy()
.ToTable("Blogs");
modelBuilder.Entity<RssBlog>()
.ToTable("RssBlogs");
Tips
I stället för att anropa ToTable för varje entitetstyp genereras tabellnamnen enligt konvention genom att endast anropa modelBuilder.Entity<Blog>().UseTpcMappingStrategy() för varje rotentitetstyp.
Tips
Information om hur du konfigurerar olika kolumnnamn för primärnyckelkolumnerna i varje tabell finns i Tabellspecifik fasetteringskonfiguration.
EF skapar följande databasschema för modellen ovan.
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-databasschema
TPC-strategin liknar TPT-strategin förutom att en annan tabell skapas för varje konkret typ i hierarkin, men tabeller skapas inte för abstrakta typer – därav namnet "table-per-concrete-type". Precis som med TPT anger själva tabellen vilken typ av objekt som sparats. Men till skillnad från TPT-mappning innehåller varje tabell kolumner för varje egenskap i betongtypen och dess bastyper. TPC-databasscheman avnormaliseras.
Överväg till exempel att mappa den här hierarkin:
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>"}";
}
När du använder SQL Server är tabellerna som skapats för den här hierarkin:
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]));
Observera att:
Det finns inga tabeller för
AnimalellerPettyper, eftersom dessa ärabstracti objektmodellen. Kom ihåg att C# inte tillåter instanser av abstrakta typer, och det finns därför ingen situation där en abstrakt typinstans sparas i databasen.Mappningen av egenskaper i bastyper upprepas för varje betongtyp. Till exempel har varje tabell en
Namekolumn, och både Katter och Hundar har enVetkolumn.Om du sparar data i den här databasen resulterar det i följande:
Cats-tabell
| Id | Namn | FoodId | Veterinär | Utbildningsnivå |
|---|---|---|---|---|
| 1 | Alice | 99ca3e98-b26d-4a0c-d4ae-08da7aca624f | Pengelly | Masterexamen i företagsekonomi (MBA) |
| 2 | Mac | 99ca3e98-b26d-4a0c-d4ae-08da7aca624f | Pengelly | Förskola |
| 8 | Baxter | 5dc5019e-6f72-454b-d4b0-08da7aca624f | Bothell Pet Hospital | Bsc |
Hundarnas tabell
| Id | Namn | FoodId | Veterinär | FavoriteToy |
|---|---|---|---|---|
| 3 | Rostat bröd | 011aaf6f-d588-4fad-d4ac-08da7aca624f | Pengelly | Herr Ekorre |
Farmdjurstabell
| Id | Namn | FoodId | Värde | Art |
|---|---|---|---|---|
| 4 | Clyde | 1d495075-f527-4498-d4af-08da7aca624f | 100.00 | Equus africanus asinus |
Humans-tabell
| Id | Namn | FoodId | FavoritDjurId |
|---|---|---|---|
| 5 | Wendy | 5418fd81-7660-432f-d4b1-08da7aca624f | 2 |
| 6 | Arthur | 59b495d4-0414-46bf-d4ad-08da7aca624f | 1 |
| 9 | Katie | noll | 8 |
Observera att till skillnad från med TPT-mappning finns all information för ett enskilt objekt i en enda tabell. Och till skillnad från med TPH-mappning finns det ingen kombination av kolumn och rad i en tabell där den aldrig används av modellen. Nedan ser vi hur dessa egenskaper kan vara viktiga för frågor och lagring.
Nyckelgenerering
Den valda arvsmappningsstrategin får konsekvenser för hur primära nyckelvärden genereras och hanteras. Nycklar i TPH är enkla eftersom varje entitetsinstans representeras av en enda rad i en enda tabell. Alla typer av nyckelvärdegenerering kan användas och inga ytterligare begränsningar behövs.
För TPT-strategin finns det alltid en rad i tabellen som mappas till bastypen för hierarkin. Alla typer av generering av nycklar kan användas på den här tabellen, och nycklarna för andra tabeller är länkade till denna tabell med hjälp av begränsningar för främmande nyckel.
Saker blir lite mer komplicerade för TPC. För det första är det viktigt att förstå att EF Core kräver att alla entiteter i en hierarki har ett unikt nyckelvärde, även om entiteterna har olika typer. Med vår exempelmodell kan en hund till exempel inte ha samma ID-nyckelvärde som en katt. För det andra finns det, till skillnad från TPT, ingen gemensam tabell som kan fungera som den enda plats där nyckelvärdena finns och kan genereras. Det innebär att det inte går att använda en enkel Identity kolumn.
För databaser som stöder sekvenser kan nyckelvärden genereras med hjälp av en enda sekvens som refereras till i standardvillkoret för varje tabell. Det här är den strategi som används i de TPC-tabeller som visas ovan, där varje tabell har följande:
[Id] int NOT NULL DEFAULT (NEXT VALUE FOR [AnimalSequence])
AnimalSequence är en databassekvens som skapats av EF Core. Den här strategin används som standard för TPC-hierarkier när du använder EF Core-databasprovidern för SQL Server. Databasprovidrar för andra databaser som stöder sekvenser bör ha ett liknande standardvärde. Andra nyckelgenereringsstrategier som använder sekvenser, till exempel Hi-Lo-mönster, kan också användas med TPC.
Standardidentitetskolumner fungerar inte med TPC, men det är möjligt att använda identitetskolumner om varje tabell har konfigurerats med ett lämpligt startvärde och öka så att de värden som genereras för varje tabell aldrig hamnar i konflikt. Till exempel:
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));
Viktigt!
Om du använder den här strategin blir det svårare att lägga till härledda typer senare eftersom det totala antalet typer i hierarkin måste vara känt i förväg.
SQLite stöder inte sekvenser eller identitetsutsäde/inkrement, och därför stöds inte heltalsnyckelvärdegenerering när du använder SQLite med TPC-strategin. Generering på klientsidan eller globalt unika nycklar , till exempel GUID: er, stöds dock på alla databaser, inklusive SQLite.
Begränsningar för främmande nyckel
TPC-mappningsstrategin skapar ett avnormaliserat SQL-schema – det är en anledning till att vissa databaspurister är emot det. Anta till exempel kolumnen för foreign key FavoriteAnimalId. Värdet i den här kolumnen måste matcha det primära nyckelvärdet för vissa djur. Detta kan tillämpas i databasen med en enkel FK-begränsning när du använder TPH eller TPT. Till exempel:
CONSTRAINT [FK_Animals_Animals_FavoriteAnimalId] FOREIGN KEY ([FavoriteAnimalId]) REFERENCES [Animals] ([Id])
Men när du använder TPC lagras primärnyckeln för ett visst djur i tabellen som motsvarar djurets betongtyp. Till exempel lagras en katts primärnyckel i kolumnen Cats.Id, medan en hunds primärnyckel lagras i kolumnen Dogs.Id och så vidare. Det innebär att det inte går att skapa en FK-begränsning för den här relationen.
I praktiken är detta inte ett problem så länge programmet inte försöker infoga ogiltiga data. Om alla data till exempel infogas av EF Core och använder navigering för att relatera entiteter, garanteras det att FK-kolumnen alltid innehåller giltiga PK-värden.
Sammanfattning och vägledning
Sammanfattningsvis är TPH vanligtvis bra för de flesta program och är en bra standard för en mängd olika scenarier, så lägg inte till komplexiteten i TPC om du inte behöver det. Mer specifikt, om koden främst frågar efter entiteter av många typer, till exempel att skriva frågor mot bastypen, lutar du dig mot TPH över TPC.
Med detta sagt är TPC också en bra mappningsstrategi att använda när din kod främst frågar efter entiteter av en enskild lövtyp och dina riktmärken visar en förbättring jämfört med TPH.
Använd endast TPT om du är tvingad till det av externa faktorer.