Code First中有三種不同的方法表示繼承層次關系
1.Table per Hierarchy (TPH):
這種方法建議用一個表來表示整個類的繼承層次關系,表中包含一個識別列來區分繼承類,在EntityFramework中,這是默認的實現
類與數據庫表的映射最簡單的策略應該是:每個持久類對應一張表。這種方法聽起來很簡單,繼承是一個可見的結構之間的不匹配的面向對象和關系世界,因為面向對象系統模型都是“is a”和“has a”的關系。
SQL中的實體關系都是“has a”的關系。SQL數據庫管理系統不支持繼承的關系
我將解釋這些策略的一個系列,這是一個致力於TPH,在這一系列中,我們將深入挖掘每一個策略,將學習“為什么”來選擇它們以及“如何”來實現它們,希望它給你一個比較好的想法在具體的場景中使用哪個策略。
所有的繼承映射策略,在這一系列的討論,我們將第一ctp5 EF代碼實現。建立新的ctp5 EF代碼庫已通過ADO.NET團隊本月早些時候公布的。EF代碼首先啟用了一個功能強大的以代碼為中心的開發工作流。我是一個大風扇的EF Code First的方法,我很興奮的生產力和權力,它帶來了很多。當涉及到繼承映射,不僅代碼第一完全支持所有的策略,但也給你最終的靈活性與涉及繼承的域模型。在ctp5繼承映射Fluent API有了很大的提高,現在它更直觀、簡潔的比較ctp4。
如果你遵循EF的“數據庫第一”或“模型第一”的方法,我仍然建議閱讀本系列,雖然實施是代碼第一具體的,但周圍的每一個戰略的解釋是完全適用於所有的方法,它的代碼第一或其他。
public abstract class BillingDetail { public int BillingDetailId { get; set; } public string Owner { get; set; } public string Number { get; set; } } public class BankAccount : BillingDetail { public string BankName { get; set; } public string Swift { get; set; } } public class CreditCard : BillingDetail { public int CardType { get; set; } public string ExpiryMonth { get; set; } public string ExpiryYear { get; set; } } public class InheritanceMappingContext : DbContext { public DbSet<BillingDetail> BillingDetails { get; set; } }
定義如上代碼,在DbContext中,只定義基類BillingDetail的DbSet,Code First通過類型發現約定找到繼承基類的類型
多態查詢:
LINQ to Entities 和 EntitySQL作為面向對象查詢語言,都支持多態的查詢,查詢這個對象的所有實例和子類的所有實例,
IQueryable<BillingDetail> linqQuery = from b in context.BillingDetails select b; List<BillingDetail> billingDetails = linqQuery.ToList();
用EntitySQL語言查詢
string eSqlQuery = @"SELECT VAlUE b FROM BillingDetails AS b"; ObjectContext objectContext = ((IObjectContextAdapter)context).ObjectContext; ObjectQuery<BillingDetail> objectQuery = objectContext.CreateQuery<BillingDetail>(eSqlQuery); List<BillingDetail> billingDetails = objectQuery.ToList();
linqQuery 和 eSqlQuery 都是多態的並返回類型是BillingDetail的對象集合(BillingDetail是抽象類),不過集合中的具體對象則是BillingDetail的具體子類:BankAccount和CreditCard
不是多態的查詢:
所有的LINQ to Entities和EntitySQL查詢都是多態的,不僅返回具體實體類的實例,也返回所有的子類。非多態查詢是多態性受限的查詢,只返回特定子類的實例。在LINQ to Entities中,用OfType<T>方法返回,下面的例子中,查詢返回的是BankCount的實例
IQueryable<BankAccount> query = from b in context.BillingDetails.OfType<BankAccount>() select b;
string eSqlQuery = @"SELECT VAlUE b FROM OFTYPE(BillingDetails, Model.BankAccount) AS b";
整個類的層次都映射到一張表中,這個表包含的列為所有類和子類的所有屬性,每個子類用特殊的列值來區分
上面的例子中,BillingDetail抽象類的屬性和對應子類BankAccount、CreditCard的屬性都映射到表中,Code First默認添加列名為Discriminator 的列來區分不同的子類(類型為不可null的nvarchar)
默認值為:BankAccount和CreditCard
TPH要求子類的屬性在數據庫表中類型為Nullable
TPH有一個主要的問題是:子類的屬性在數據庫表中的映射類型是Nullable的,例如,Code First創建一列(INT,NULL)對應CreditCard類中的CardType屬性,然而,在一個特別的映射場景中,Code First總是創建一列(INT,Not Null)對應實體中的int類型屬性。但在這個例子中,BankAccount實例沒有CardType屬性,在這一行CardType必須為NULL,Code Fist用(INT,NOT NULL)代替。
如果子類中有定義為non-nullable的屬性,NOT NULL轉換將出現異常
另一個問題是:TPH違反第三范式
決定鑒別器列中的值的列,屬於子類的相應值(例如bankname)但鑒頻器是不是該表的主鍵的一部分。
當查詢BillingDetails 時,生成如下SQL語句
SELECT [Extent1].[Discriminator] AS [Discriminator], [Extent1].[BillingDetailId] AS [BillingDetailId], [Extent1].[Owner] AS [Owner], [Extent1].[Number] AS [Number], [Extent1].[BankName] AS [BankName], [Extent1].[Swift] AS [Swift], [Extent1].[CardType] AS [CardType], [Extent1].[ExpiryMonth] AS [ExpiryMonth], [Extent1].[ExpiryYear] AS [ExpiryYear] FROM [dbo].[BillingDetails] AS [Extent1] WHERE [Extent1].[Discriminator] IN ('BankAccount','CreditCard')
查詢BankAccount 時,生成如下語句
SELECT [Extent1].[BillingDetailId] AS [BillingDetailId], [Extent1].[Owner] AS [Owner], [Extent1].[Number] AS [Number], [Extent1].[BankName] AS [BankName], [Extent1].[Swift] AS [Swift] FROM [dbo].[BillingDetails] AS [Extent1] WHERE [Extent1].[Discriminator] = 'BankAccount'
用Fluent API配置識別列
protected override void OnModelCreating(DbModelBuilder modelBuilder) { modelBuilder.Entity<BillingDetail>() .Map<BankAccount>(m => m.Requires("BillingDetailType").HasValue("BA")) .Map<CreditCard>(m => m.Requires("BillingDetailType").HasValue("CC")); }
modelBuilder.Entity<BillingDetail>() .Map<BankAccount>(m => m.Requires("BillingDetailType").HasValue(1)) .Map<CreditCard>(m => m.Requires("BillingDetailType").HasValue(2));
2.Table per Type (TPT):
這種方法建議用分開的表來表示每個領域里面的類
Table per Type用關系外鍵表示繼承關系
每一個類、子類、抽象類都有它們自己的表
子類的表包含的列僅僅是非繼承的屬性(子類中自己聲明的屬性),子類表的主鍵是基類的主鍵
例如,如果子類CreditCard持久化,BillingDetail基類中的屬性值將被持久化到BillingDetail表中,CreditCard子類中聲明的屬性被持久化到CreditCard表中,兩個表中對應的這兩行將共享一個主鍵
最后,查詢子類實例時將聯合子類表和基類表進行查詢
TPT的好處有:
主要的好處是這符合SQL規范模式,
Code First實現TPT
public abstract class BillingDetail { public int BillingDetailId { get; set; } public string Owner { get; set; } public string Number { get; set; } } [Table("BankAccounts")] public class BankAccount : BillingDetail { public string BankName { get; set; } public string Swift { get; set; } } [Table("CreditCards")] public class CreditCard : BillingDetail { public int CardType { get; set; } public string ExpiryMonth { get; set; } public string ExpiryYear { get; set; } } public class InheritanceMappingContext : DbContext { public DbSet<BillingDetail> BillingDetails { get; set; } }
protected override void OnModelCreating(DbModelBuilder modelBuilder) { modelBuilder.Entity<BankAccount>().ToTable("BankAccounts"); modelBuilder.Entity<CreditCard>().ToTable("CreditCards"); }
多態關聯:是一個基類的關聯,因此說有的層次類都是在運行時才能確定是哪個具體的類
例如,User類中的BillingInfo屬性,它引用一個BillingDetail對象,運行時,這個屬性可以是這個類的任何一個具體實例
public class User { public int UserId { get; set; } public string FirstName { get; set; } public string LastName { get; set; } public int BillingDetailId { get; set; } public virtual BillingDetail BillingInfo { get; set; } }
using (var context = new InheritanceMappingContext()) { CreditCard creditCard = new CreditCard() { Number = "987654321", CardType = 1 }; User user = new User() { UserId = 1, BillingInfo = creditCard }; context.Users.Add(user); context.SaveChanges(); }
查詢
var query = from b in context.BillingDetails.OfType<BankAccount>() select b;
var query = from b in context.BillingDetails select b;
As you can see, EF Code First relies on an INNER JOIN to detect the existence (or absence) of rows in the subclass tables CreditCards and BankAccounts so it can determine the concrete subclass for a particular row of the BillingDetails table. Also the SQL CASE statements that you see in the beginning of the query is just to ensure columns that are irrelevant for a particular row have NULL values in the returning flattened table. (e.g. BankName for a row that represents a CreditCard type)
TPT考慮到的問題:
雖然這看似簡單的映射策略,經驗表明,復雜的類層次結構的性能是不能接受的,因為查詢總是需要跨多個表的聯接,另外,這種映射策略更難以手工實現,
這是一個重要的考慮因素,如果你打算使用手寫SQL在您的應用程序,專案報告,數據庫視圖提供了一種方法來抵消TPT策略的復雜性
3.Table per Concrete class (TPC):
這種方法建議用一個表表示一個具體的類。但是不表示抽象方法。所以,如果在多個具體類中繼承抽象方法,抽象類中的屬性將是具體類對應表中的一部分
為每個具體的類型創建一張表(不包括抽象類),類中的所有屬性(包括繼承屬性),都會映射到表中的列
如下
如上面所示,SQL不知道繼承,事實上,我們將兩個無關的表映射成更具表達性的類結構,如果基類是具體的,則需要額外的表來控制類的實例,必須強調的是這些表是沒有什么關聯的,實際上除了它們共享的一些列
就像TPT一樣,我們需要為每個子類指定分開的表。我們也需要告訴Code First所有的繼承屬性都映射到表中,
public abstract class BillingDetail { public int BillingDetailId { get; set; } public string Owner { get; set; } public string Number { get; set; } } public class BankAccount : BillingDetail { public string BankName { get; set; } public string Swift { get; set; } } public class CreditCard : BillingDetail { public int CardType { get; set; } public string ExpiryMonth { get; set; } public string ExpiryYear { get; set; } } public class InheritanceMappingContext : DbContext { public DbSet<BillingDetail> BillingDetails { get; set; } protected override void OnModelCreating(DbModelBuilder modelBuilder) { modelBuilder.Entity<BankAccount>().Map(m => { m.MapInheritedProperties(); m.ToTable("BankAccounts"); }); modelBuilder.Entity<CreditCard>().Map(m => { m.MapInheritedProperties(); m.ToTable("CreditCards"); }); } }
namespace System.Data.Entity.ModelConfiguration.Configuration.Mapping { public class EntityMappingConfiguration<TEntityType> where TEntityType : class { public ValueConditionConfiguration Requires(string discriminator); public void ToTable(string tableName); public void MapInheritedProperties(); } }
using (var context = new InheritanceMappingContext()) { BankAccount bankAccount = new BankAccount(); CreditCard creditCard = new CreditCard() { CardType = 1 }; context.BillingDetails.Add(bankAccount); context.BillingDetails.Add(creditCard); context.SaveChanges(); }
運行上面的代碼發生如下異常
The changes to the database were committed successfully, but an error occurred while updating the object context. The ObjectContext might be in an inconsistent state. Inner exception message: AcceptChanges cannot continue because the object's key values conflict with another object in the ObjectStateManager. Make sure that the key values are unique before calling AcceptChanges.
該原因是因為DbContext.SaveChanges()內部調用了ObjectContext的SaveChanges方法,ObjectContext的調用SaveChanges方法對其將默認調用acceptallchanges后已完成數據庫的修改,acceptallchanges方法只遍歷所有條目在ObjectStateManager並調用AcceptChanges對它們
當實體處於Added狀態,AcceptChanges 方法用數據中生成的主鍵來替換它們的臨時EntityKey,因此當所有實體被賦予相同的主鍵值時會發生異常,
問題是ObjectStateManager 不能跟蹤類型相同且主鍵相同的對象,打開數據庫的表看到的數據是BankAccounts表和CreditCards表有相同的主鍵值
如何解決 TPC中的ID問題
如你看到的,SQL Server中的int型標識列不能與TPC很好的一起工作,當插入到子類表中時會復制實體的主鍵,
因此,解決該問題,可以使用連續的種子,(每個表都有自己的初始種子),使用GUID或者int標識列(以不同的種子開頭)可以解決該問題,
public abstract class BillingDetail { [DatabaseGenerated(DatabaseGenerationOption.None)] public int BillingDetailId { get; set; } public string Owner { get; set; } public string Number { get; set; } }
modelBuilder.Entity<BillingDetail>() .Property(p => p.BillingDetailId) .HasDatabaseGenerationOption(DatabaseGenerationOption.None);
using (var context = new InheritanceMappingContext()) { BankAccount bankAccount = new BankAccount() { BillingDetailId = 1 }; CreditCard creditCard = new CreditCard() { BillingDetailId = 2, CardType = 1 }; context.BillingDetails.Add(bankAccount); context.BillingDetails.Add(creditCard); context.SaveChanges(); }
如果User中有BillingDetail的引用,查詢的話會生成如下sql語句
如上所示,將兩個子類的表進行聯合查詢Union All
選擇策略指南
我想強調的是,沒有一個單一的“最佳策略適合所有場景”存在
每種方法都有各自的優點和缺點
如果確實不需要多態關聯或查詢,用TPC(換句話說,如果你不查詢或者很少查詢BillingDetails ,並且你沒有與BillingDetail基類相關聯的類),推薦用TPC,
如果你需要多態關聯或查詢,和子類聲明相對較少的性能(特別是如果子類之間的主要區別是他們的行為),傾向於TPH。你的目標是減少可為空的列數和說服自己(和你的DBA),一個規范化的模式不會產生長期的問題。
如果你需要多態關聯或查詢,和子類(子類聲明的許多性質不同主要由他們所持有的數據),傾向於TPT。或者,取決於繼承層次結構的寬度和深度,以及聯接與工會的可能成本,請使用TPC。