前言 最近因為看了一些有關DDD領域驅動設計的文章,於是就很好奇,如何將充血模型映射至資料庫,其中物件的繼承關係複雜時,在資料庫會是甚麼樣的結構,通過EF做查詢SQL語法會如何做轉換,所以打算親自實驗看看。
範例說明
Github範例程式https://github.com/DeliaHung/EFcore-Inheritance
本篇範例參考微軟官方文件做一點簡化調整https://learn.microsoft.com/zh-tw/ef/core/modeling/inheritance
新增Animal、Pet抽象類別。
Cat、Dog繼承Pet,Human繼承Animal。
Cat、Dog、Human有相同屬性 Species(物種)。
Cat、Dog、Human有各自不同屬性。
Cat、Dog都有自己的主人,Human也有多個寵物,屬於多對多關係。
建立領域物件 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 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 Cat : Pet { public Cat (string name, int educationLevel ) : base (name ) { EducationLevel = educationLevel; } public int EducationLevel { get ; set ; } public override string Species => "Felis catus" ; public override string ToString () => $"Cat '{Name} ' ({Species} /{Id} ) with education '{EducationLevel} '" ; } 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} '" ; } 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>" } '" ; }
建立DbContext public class TestContext : DbContext { public DbSet<Animal> Animals { get ; set ; } public DbSet<Pet> Pets { get ; set ; } public DbSet<Cat> Cats { get ; set ; } public DbSet<Dog> Dogs { get ; set ; } public DbSet<Human> Humans { get ; set ; } protected override void OnConfiguring (DbContextOptionsBuilder optionsBuilder ) { optionsBuilder.UseSqlServer("Data Source=DESKTOP-9F0J9EK;Initial Catalog=EFInheritanceTPT;Integrated Security=True;TrustServerCertificate=True" ); } protected override void OnModelCreating (ModelBuilder builder ) { builder.ApplyConfigurationsFromAssembly(Assembly.GetExecutingAssembly()); base .OnModelCreating(builder); } }
為了方便測試及觀察產生的SQL語法,這邊使用LinqPad做實驗。
LinqPad Insert測試
LinqPad Search測試
Code-First 繼承映射的三種方式 1.TPH(Table per Class Hierarchy)
EF Core預設
一張表存放父類和子類的所有屬性,並自動生成識別器(discriminator)欄位,用來區分基類和各個子類的。優點是查詢速度快, 缺點是數據庫結構有點亂, 因為它把全部東西都塞進一個表, 會有很多nullable。
資料表結構 預設產生出來的Discriminator,預設為string,存入時就是子類的名稱。
設定
使用Fluent API添加配置 在父類別加上UseTphMappingStrategy()。
識別器可以自己指定Type及Value,也可將識別器寫在父類屬性中,依照你的程式設計需求。
如果資料表有識別器無法識別的Type,會造成查詢時發生錯誤,以下例子是我自己去資料庫把Human的Type改成Type2。我們可以在設定加上IsComplete(false),詳細設定請參考下面程式碼。
public class AnimalConfiguration : IEntityTypeConfiguration <Animal >{ public void Configure (EntityTypeBuilder<Animal> builder ) { builder.HasKey(t => t.Id); builder.Property(t => t.Id).UseIdentityColumn(1 , 1 ); builder.UseTphMappingStrategy() .HasDiscriminator().IsComplete(false ); builder.HasDiscriminator<int >("AnimalType" ) .HasValue<Cat>(1 ) .HasValue<Dog>(2 ) .HasValue<Human>(3 ); builder.HasDiscriminator(d => d.AnimalType) .HasValue<Cat>(1 ) .HasValue<Dog>(2 ) .HasValue<Human>(3 ); } }
TPH寫入結果
TPH查詢測試
2.TPT(Table per Type) 在TPT模式中,不管基底類別還是子類別通通都會建立一張表,這種模式下,每個表格都有一個主鍵,並且關聯的資料存放在各自對應的表格中,優點是數據庫結構好看, 不像 TPH 全部塞一個表, 又一堆 nullable. 缺點就是 query 慢. 因為每次 query 都需要 join。
資料表結構
設定
使用Fluent API添加配置,在父類別上加上UseTptMappingStrategy(),或是子類別全部指定Table名稱。
public class AnimalConfiguration : IEntityTypeConfiguration <Animal >{ public void Configure (EntityTypeBuilder<Animal> builder ) { builder.HasKey(t => t.Id); builder.Property(t => t.Id).UseIdentityColumn(1 , 1 ); builder.UseTptMappingStrategy(); } } public class PetConfiguration : IEntityTypeConfiguration <Pet >{ public void Configure (EntityTypeBuilder<Pet> builder ) { builder.ToTable(nameof (Pet)); } } public class CatConfiguration : IEntityTypeConfiguration <Cat >{ public void Configure (EntityTypeBuilder<Cat> builder ) { builder.ToTable(nameof (Cat)); } } public class DogConfiguration : IEntityTypeConfiguration <Dog >{ public void Configure (EntityTypeBuilder<Dog> builder ) { builder.ToTable(nameof (Dog)); } } public class HumanConfiguration : IEntityTypeConfiguration <Human >{ public void Configure (EntityTypeBuilder<Human> builder ) { builder.ToTable(nameof (Human)); } }
TPT寫入結果
TPT查詢測試
3.TPC(Table per Concrete Class) TPC是EF7.0才推出的,TPC 類似于 TPT 策略,不同資料表是針對階層中的每個類別所建立,但 不會針對抽象類別建立資料表
,基底類別中的屬性,會存在所有子類資料表裡面。 例如,每個資料表都有 Id、Name 。
資料表結構
設定
使用Fluent API添加配置,在父類別上加上UseTpcMappingStrategy()
注意父類別Id不可使用SQL自增流水號,會造成Id重複導致Insert錯誤。
public class AnimalConfiguration : IEntityTypeConfiguration <Animal >{ public void Configure (EntityTypeBuilder<Animal> builder ) { builder.HasKey(t => t.Id); builder.UseTpcMappingStrategy(); } }
SEQUENCE
EF Core 要求所有實體都有唯一Key值,即使實體的類型不同也一樣,例如,Dog 不能有與 Cat 相同Key值,這表示無法使用簡單的 Identity 資料行。
SQLite 不支援序列或身分識別種子/遞增,因此搭配 TPC 策略使用 SQLite 時,不支援產生整數索引鍵值。 不過,任何資料庫都支援用戶端產生或全域唯一索引鍵,例如 GUID,包括 SQLite。
如果不想用GUID,也可使用Hi-Lo模式。https://vladmihalcea.com/the-hilo-algorithm/ https://learn.microsoft.com/zh-tw/dotnet/architecture/microservices/microservice-ddd-cqrs-patterns/infrastructure-persistence-layer-implementation-entity-framework-core
TPC寫入結果
TPC查詢測試
結論 複習一下三種結構差異
Table per Type (TPT): TPT模式適合在需要對繼承層次結構進行頻繁更改和擴展的情況下。每個類別都有自己獨立的資料表,讓您可以獨立管理和操作每個類別的資料。這種模式較為正規化,因為每個資料表只包含特定類別的屬性。然而,這種模式可能導致較多的資料表格, 需要進行較多的 JOIN 操作
,並且資料庫結構相對複雜,管理和維護成本較高。
Table per Hierarchy (TPH): TPH模式適合在繼承層次結構相對穩定且不太會經常更改的情況下。整個繼承層次結構映射到單一資料表格,使用類型標誌(type discriminator)區分不同的類別。這種模式有 較好的查詢效能和正規化特性
,避免了較多的 JOIN 操作。然而,較大的資料表格可能包含一些不使用或為空的欄位,而且在繼承層次結構變動時,需要調整類型標誌。
以下為官方結論:
總而言之,TPH 通常適用于大部分的應用程式,如果不需要 TPC,請勿新增複雜度。如果程式碼大部分會查詢許多類型的實體,例如針對基底類型查詢,TPH會是比較好的選擇。 換句話說,當您的程式碼大部分會查詢單一類型的實體,TPC 是一個良好的選擇。 只有在受限於外部因素的情況下,才使用 TPT。
效能比較 https://davecallan.com/entity-framework-7-inheritance-mapping-performance-benchmarks/
TPH優先選擇,如果欄位真的太多太臃腫,而且太多nullable不太直覺,可以考慮換成TPC ,尤其在業務需求通常都是指定查詢某個子類別,那麼可以優先考慮TPC.
參考文章 https://learn.microsoft.com/zh-tw/ef/core/modeling/inheritance https://learn.microsoft.com/zh-tw/ef/core/performance/modeling-for-performance#inheritance-mapping https://www.entityframeworktutorial.net/code-first/inheritance-strategy-in-code-first.aspx https://www.entityframeworktutorial.net/code-first/inheritance-strategy-in-code-first.aspx https://www.cnblogs.com/dotnet261010/p/8018266.html