共用方式為


遺產

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 對應時,資料庫欄會在必要時自動設為可空值。 例如,RssUrl 數據行可為 Null,因為一般 Blog 實例沒有該屬性。

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

小提示

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

每一階層的數據表和歧視性設定

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

上述模型會對應至下列資料庫綱要(請注意隱含建立的 Discriminator 欄位,此欄位用於識別每一行中儲存的 Blog 類型)。

使用數據表個別階層模式查詢部落格實體階層結果的螢幕快照

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

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("blog_type")
        .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 對應模式中,所有類型都會對應至個別數據表。 只屬於基底類型或衍生型別的屬性會儲存在對應至該類型的數據表中。 對應至衍生型別的數據表也會儲存外鍵,以聯結衍生數據表與基表。

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

小提示

您可以在每個根實體類型上呼叫 ToTable,這樣數據表名稱將由 EF 產生,而不是在每個實體類型上分別呼叫 modelBuilder.Entity<Blog>().UseTptMappingStrategy()

小提示

若要設定每個資料表中主鍵資料列的不同資料行名稱,請參閱 資料表特定的 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 條件約束和索引。

各具體類型的單獨數據表配置

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

請注意:

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

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

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

Cats 資料表

身份識別碼 名稱 FoodId 獸醫 教育程度
1 愛麗絲 99ca3e98-b26d-4a0c-d4ae-08da7aca624f Pengelly MBA(企管碩士)
2 Mac(蘋果電腦) 99ca3e98-b26d-4a0c-d4ae-08da7aca624f Pengelly 幼稚園
8 巴克斯特 5dc5019e-6f72-454b-d4b0-08da7aca624f 博瑟爾寵物醫院 理學士

Dogs 的資料表

身份識別碼 名稱 FoodId 獸醫 FavoriteToy
3 吐司 011aaf6f-d588-4fad-d4ac-08da7aca624f Pengelly 松鼠先生

農場動物數據表

身份識別碼 名稱 FoodId 價值 物種
4 克萊德 1d495075-f527-4498-d4af-08da7aca624f 100.00 equus africanus asinus

人類數據表

身份識別碼 名稱 FoodId FavoriteAnimalId (最愛動物ID)
5 溫迪 5418fd81-7660-432f-d4b1-08da7aca624f 2
6 亞瑟 59b495d4-0414-46bf-d4ad-08da7aca624f 1
9 凱蒂 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 時,不支援產生整數索引鍵值。 不過,任何資料庫,包括 SQLite,都支援用戶端生成或全域唯一的索引鍵,例如 GUID。

外來鍵約束

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。