繼承

EF 可以將 .NET 類型階層對應至資料庫。 這可讓您照常在程式碼中撰寫 .NET 實體,使用基底和衍生類型,並讓 EF 順暢地建立適當的資料庫架構、發出查詢等。型別階層對應方式的實際詳細資料與提供者相依;此頁面描述關係資料庫內容中的繼承支援。

實體類型階層對應

根據慣例,EF 不會自動掃描基底或衍生類型;這表示如果您想要對應階層中的 CLR 類型,您必須在模型上明確指定該類型。 例如,只指定階層的基底類型,不會讓 EF Core 隱含地包含其所有子類型。

下列範例會公開 的 Blog DbSet 及其子類別 RssBlog 。 如果有 Blog 任何其他子類別,則不會包含在模型中。

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

注意

使用 TPH 對應時,資料庫資料行會自動設為可為 Null。 例如,資料 RssUrl 行可為 Null,因為一般 Blog 實例沒有該屬性。

如果您不想在階層中公開 DbSet 一或多個實體的 ,您也可以使用 Fluent API 來確保它們包含在模型中。

提示

如果您不依賴 慣例 ,則可以使用 HasBaseType 明確指定基底類型。 您也可以使用 .HasBaseType((Type)null) 從階層中移除實體類型。

每一階層的資料表和歧視性設定

根據預設,EF 會使用 每個階層 的資料表模式來對應繼承。 TPH 會使用單一資料表來儲存階層中所有型別的資料,並使用歧視性資料行來識別每個資料列所代表的類型。

上述模型會對應至下列資料庫架構(請注意隱含建立 Discriminator 的資料行,可識別每個資料列中儲存的 類型 Blog )。

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

您可以設定歧視性資料行的名稱和類型,以及用來識別階層中每個類型的值:

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

在上述範例中,EF 會在階層的基底實體上隱含地 新增歧視性作為陰影屬性 。 這個屬性可以像任何其他屬性一樣進行設定:

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

最後,此歧視性也可以對應至實體中的一般 .NET 屬性:

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

針對使用 TPH 模式的衍生實體進行查詢時,EF Core 會在查詢中新增對歧視性資料行的述詞。 此篩選可確保我們不會針對基底類型或非結果中的同層級類型取得任何其他資料列。 此篩選述詞會針對基底實體類型略過,因為查詢基底實體將會取得階層中所有實體的結果。 當從查詢具體化結果時,如果我們遇到不對應至模型中任何實體類型的歧視性值,我們會擲回例外狀況,因為我們不知道如何具體化結果。 只有當資料庫包含具有歧視性值的資料列時,才會發生此錯誤,而該資料列未對應到 EF 模型中。 如果您有這類資料,您可以將 EF Core 模型中的歧視性對應標示為不完整,以指出我們一律應該新增篩選述詞來查詢階層中的任何類型。 IsComplete(false) 對歧視性組態的呼叫會將對應標示為不完整。

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

共用資料行

根據預設,當階層中的兩個同層級實體類型具有相同名稱的屬性時,它們會對應至兩個不同的資料行。 不過,如果其類型相同,則可以對應至相同的資料庫資料行:

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

注意

SQL Server 等關係資料庫提供者在使用轉換時查詢共用資料行時,不會自動使用歧視性述詞。 Url = (blog as RssBlog).Url查詢也會傳回 Url 同層級 Blog 資料列的值。 若要將查詢限制為 RssBlog 實體,您必須手動在歧視性上新增篩選,例如 Url = blog is RssBlog ? (blog as RssBlog).Url : null

每一類型的資料表組態

在 TPT 對應模式中,所有類型都會對應至個別資料表。 單獨屬於基底型別 (Base Type) 或衍生型別 (Derived Type) 的屬性會儲存在對應至該型別的資料表中。 對應至衍生型別的資料表也會儲存外鍵,以聯結衍生資料表與基表。

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

提示

ToTable您可以呼叫每個根實體類型,而不是在每個實體類型上呼叫 modelBuilder.Entity<Blog>().UseTptMappingStrategy() ,而且資料表名稱將由 EF 產生。

提示

若要為每個資料表中的主鍵資料行設定不同的資料行名稱,請參閱 資料表特定的 Facet 組態

EF 會為上述模型建立下列資料庫架構。

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

注意

如果主鍵條件約束重新命名,新名稱將會套用至對應至階層的所有資料表,未來的 EF 版本將允許在修正 19970 問題時 ,只針對特定資料表重新命名條件約束。

如果您要採用大量設定,您可以藉由呼叫 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();
}

警告

在許多情況下,相較于 TPH,TPT 會顯示劣質效能。 如需詳細資訊 ,請參閱效能檔。

警告

衍生類型的資料行會對應至不同的資料表,因此無法在資料庫中建立使用繼承和宣告屬性的複合 FK 條件約束和索引。

資料表個別具象類型組態

注意

EF Core 7.0 中引進了每個具體類型的資料表 (TPC) 功能。

在 TPC 對應模式中,所有類型都會對應至個別資料表。 每個資料表都包含對應實體類型上所有屬性的資料行。 這解決了 TPT 策略的一些常見效能問題。

提示

EF 小組在 .NET 資料社群月臺 的情節 中示範並深入討論 TPC 對應。 和所有社群站立劇集一樣,您現在可以 在 YouTube 上觀看 TPC 劇集。

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

提示

而不是在每個實體類型上呼叫 ToTable ,而只是在每個根實體類型上呼叫 modelBuilder.Entity<Blog>().UseTpcMappingStrategy() ,將會依慣例產生資料表名稱。

提示

若要為每個資料表中的主鍵資料行設定不同的資料行名稱,請參閱 資料表特定的 Facet 組態

EF 會為上述模型建立下列資料庫架構。

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 資料庫架構

TPC 策略與 TPT 策略類似,不同資料表是針對 階層中的每個具體 類型所建立,但不會針對 抽象 類型建立資料表 ,因此名稱為「table-per-concrete-type」。 如同 TPT,資料表本身會指出儲存的物件類型。 不過,不同于 TPT 對應,每個資料表都包含具體類型及其基底類型中每個屬性的資料行。 TPC 資料庫架構會反正規化。

例如,請考慮對應此階層:

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 時,為此階層建立的資料表如下:

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

請注意:

  • Pet 類型沒有資料表 Animal ,因為這些資料表位於 abstract 物件模型中。 請記住,C# 不允許抽象類別型的實例,因此沒有將抽象類別型實例儲存至資料庫的情況。

  • 基底類型中的屬性對應會針對每個具體類型重複。 例如,每個資料表都有一個資料行 Name ,而 Cat 和 Dog 都有一個資料行 Vet

  • 將一些資料儲存到此資料庫中會產生下列結果:

Cats 表格

Id 名稱 FoodId 獸醫 EducationLevel
1 Alice 99ca3e98-b26d-4a0c-d4ae-08da7aca624f Pengelly Mba
2 Mac 99ca3e98-b26d-4a0c-d4ae-08da7aca624f Pengelly 學齡 前
8 巴克斯特 5dc5019e-6f72-454b-d4b0-08da7aca624f 雙塞爾寵物醫院 BSc

狗桌

Id 名稱 FoodId 獸醫 FavoriteToy
3 吐 司 011aaf6f-d588-4fad-d4ac-08da7aca624f Pengelly 松鼠先生

FarmAnimals 資料表

Id 名稱 FoodId 種類
4 克萊德 1d495075-f527-4498-d4af-08da7aca624f 100.00 equus africanus asinus

人類資料表

Id 名稱 FoodId FavoriteAnimalId
5 溫蒂 5418fd81-7660-432f-d4b1-08da7aca624f 2
6 Arthur 59b495d4-0414-46bf-d4ad-08da7aca624f 1
9 凱蒂 null 8

請注意,與 TPT 對應不同,單一物件的所有資訊都包含在單一資料表中。 而且,與 TPH 對應不同,在模型從未使用的任何資料表中,沒有資料行和資料列的組合。 我們將在下面看到這些特性對於查詢和儲存體的重要性。

金鑰產生

選擇的繼承對應策略會產生和管理主鍵值的方式。 TPH 中的索引鍵很簡單,因為每個實體實例都是以單一資料表中的單一資料清單示。 您可以使用任何類型的索引鍵值產生,而且不需要額外的條件約束。

針對 TPT 策略,資料表中一律會有一個資料列對應至階層的基底類型。 此資料列可以使用任何類型的金鑰產生,而其他資料表的索引鍵會使用外鍵條件約束連結至此資料表。

TPC 的情況會變得更複雜一點。 首先,請務必瞭解 EF Core 要求階層中的所有實體都有唯一的索引鍵值,即使實體具有不同的類型也一樣。 例如,使用我們的範例模型,Dog 不能有與 Cat 相同的識別碼索引鍵值。 其次,與 TPT 不同,沒有一個通用資料表可以做為索引鍵值存住且可以產生的單一位置。 這表示無法使用簡單的 Identity 資料行。

對於支援時序的資料庫,可以使用每個資料表之預設條件約束中所參考的單一序列來產生索引鍵值。 這是上述 TPC 資料表中使用的策略,其中每個資料表都有下列專案:

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

AnimalSequence 是 EF Core 所建立的資料庫序列。 使用適用于 SQL Server 的 EF Core 資料庫提供者時,預設會針對 TPC 階層使用此策略。 支援順序之其他資料庫的資料庫提供者應該有類似的預設值。 其他使用序列的重要產生策略,例如 Hi-Lo 模式,也可以與 TPC 搭配使用。

雖然標準身分識別資料行不適用於 TPC,但如果每個資料表都設定了適當的種子且遞增,則有可能使用 Identity 資料行,讓每個資料表產生的值永遠不會衝突。 例如:

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

重要

使用此策略可讓稍後更難新增衍生類型,因為它需要事先知道階層中的類型總數。

SQLite 不支援序列或身分識別種子/增量,因此搭配 TPC 策略使用 SQLite 時,不支援產生整數索引鍵值。 不過,任何資料庫都支援用戶端產生或全域唯一索引鍵,例如 GUID,包括 SQLite。

外鍵條件約束

TPC 對應策略會建立反正規化的 SQL 架構, 這是某些資料庫純粹主義者反對它的原因之一。 例如,請考慮外鍵資料行 FavoriteAnimalId 。 此資料行中的值必須符合某些動物的主鍵值。 使用 TPH 或 TPT 時,可以使用簡單的 FK 條件約束在資料庫中強制執行。 例如:

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

但是,使用 TPC 時,任何指定動物的主要索引鍵會儲存在對應到該動物的具體類型資料表中。 例如,貓的主鍵會儲存在資料行中 Cats.Id ,而狗的主鍵則儲存在資料行中 Dogs.Id 等等。 這表示無法為此關聯性建立 FK 條件約束。

實際上,只要應用程式未嘗試插入不正確資料,就不是問題。 例如,如果 EF Core 插入所有資料並使用導覽來關聯實體,則保證 FK 資料行會隨時包含有效的 PK 值。

摘要和指引

總而言之,TPH 通常適用于大部分的應用程式,而且是各種案例的良好預設值,因此,如果您不需要 TPC,請勿新增 TPC 的複雜性。 具體而言,如果您的程式碼大多會查詢許多類型的實體,例如針對基底類型撰寫查詢,則傾向于 TPH over TPC。

也就是說,當您的程式碼大多會查詢單一分葉類型的實體時,TPC 也是使用的良好對應策略,而您的基準測試與 TPH 相比會顯示改善。

只有在受外部因素限制的情況下,才使用 TPT。