上次的日記中已經提前預告了將要介紹的內容,在本次日記中我將介紹Entity Framework Code First如何處理類之間的繼承關系。Entity Framework Code First有三種處理類之間繼承關系的方法,我們將逐一介紹這三種處理方法。
1.Table Per Hierarchy(TPH): 只建立一個表,把基類和子類中的所有屬性都映射為表中的列。
2.Table Per Type(TPT): 為基類和每個子類建立一個表,每個與子類對應的表中只包含子類特有的屬性對應的列。
3.Table Per Concrete Type(TPC):為每個子類建立一個表,每個與子類對應的表中包含基類的屬性對應的列和子類特有屬性對應的列。
1.Table Per Hierarchy(TPH)
在這種處理方式中,Entity Framework Code First為基類和所有子類建立一個表,基類和子類中的所有屬性都映射為表中的一個列。Entity Framework Code First默認在這個表中建立一個叫做Discriminator的列,類型是nvarchar,長度是128。Entity Framework Code First會在存儲基類或子類的時候,把類名作為Discriminator列的值。
在我們前面的示例程序中,由於我們要記錄訂單是被誰創建的,以及是被誰批准的,我們新增了一個SalesPerson類。
public class SalesPerson { public string EmployeeID { get; set; } public string Name { get; set; } public string Gender { get; set; } public DateTime HiredDate { get; set; } }
並且在Order類中增加了兩個SalesPerson的實例用於記錄訂單的創建人和批准人。
public SalesPerson CreatedBy { get; set; } public SalesPerson ApprovedBy { get; set; }
我們后來細化了我們的業務流程:訂單是由銷售員創建的;當客戶要求的訂單折扣過高時,需要銷售經理的審批;經理每個月都有固定的折扣審批總額。銷售員和銷售經理都屬於銷售人員。這是一個典型的繼承關系。
根據我們細化之后的業務流程,我們創建了兩個新的類, SalesMan和SalesManager
public class SalesMan : SalesPerson { public decimal DiscountLimit { get; set; } }
public class SalesManager : SalesPerson { public decimal DiscountAmountPerMonth { get; set; } }
由於創建訂單的時候涉及到了復雜的業務邏輯,需要為訂單指定Customer和SalesMan, 我們新建了一個factory類用於創建訂單。
public static class OrderFactory { public static Order CreateNewOrder(Customer customer, SalesMan createUser) { Order order = new Order(); order.Customer = customer; order.CreatedDate = DateTime.Now; order.CreatedBy = createUser; order.ApprovedBy = null; return order; } }
我們新建一個單元測試方法用於測試我們新的銷售人員繼承關系以及新的訂單factory類。
[TestMethod] public void CanAddOrderWithSalesMan() { OrderSystemContext unitOfWork = new OrderSystemContext(); ProductRepository productRepository = new ProductRepository(unitOfWork); OrderRepository orderRepository = new OrderRepository(unitOfWork); CustomerRepository customerRepository = new CustomerRepository(unitOfWork); SalesMan salesman = new SalesMan { EmployeeID = "2012001", Gender = "M", Name = "Eric", HiredDate = DateTime.Parse("2010-5-19") }; Customer customer = customerRepository.GetCustomerById("120104198403082113"); Order order = OrderFactory.CreateNewOrder(customer, salesman); order.AddNewOrderItem(productRepository.GetProductCatalogById(1).GetProductInStock(), 5100); orderRepository.AddNewOrder(order); unitOfWork.CommitChanges(); }
執行完我們的測試程序之后,我們可以打開SQL Server去看一下Code First默認情況下是如何處理類之間的繼承關系的。
Code First默認會把基類和子類的所有屬性都映射成一個表中的列,並且會增加一個Discriminator列標識存進去的是哪個類的實例。
如果你不喜歡Discriminator這個有點奇怪的名字,你可以自己定義Discriminator列的名字以及它的類型。我們使用map方法定義該列的名字和類型。我們可以將它命名為Title。
public class SalesPersonValueObjectConfiguration: EntityTypeConfiguration<SalesPerson> { public SalesPersonValueObjectConfiguration() { HasKey(p => p.EmployeeID).Property(p => p.EmployeeID).HasDatabaseGeneratedOption(DatabaseGeneratedOption.None); Property(p => p.Name).IsRequired().HasMaxLength(100); Property(p => p.Gender).IsRequired().HasMaxLength(1); Map<SalesMan>(salesman => { salesman.Requires("Title").HasValue("SalesMan"); }); Map<SalesManager>(manager => { manager.Requires("Title").HasValue("Sales Manager"); }); } }
Map方法中傳入的類型參數是子類的類名,Requires用於指定Discriminator列的名字,HasValue用於指定它的類型和每個子類對應的值。
我們可以重新執行我們的測試程序,然后打開SQL Server,去看一下新建的數據庫表結構。
這個列的類型不僅可以是字符串,還可以是bit標志位,比如說我們把區分salesman和salemanager的列設為bit型,列的名字叫做IsManager.
Map<SalesMan>(salesman => { salesman.Requires("IsManager").HasValue(false); }); Map<SalesManager>(manager => { manager.Requires("IsManager").HasValue(true); });
我們只需要把HasValue中傳入的值變為true和false,Code First會自動把IsManager列的類型設置為bit。
2.Table Per Type(TPT)
在這種處理方式中,Entity Framework Code First會為每個基類和子類建立一個表,子類的表中只包含子類特有的屬性。
我們可以使用Map方法強制讓Code First使用TPT方式,因為Code First默認使用的是TPC方式。
public class SalesPersonValueObjectConfiguration: EntityTypeConfiguration<SalesPerson> { public SalesPersonValueObjectConfiguration() { HasKey(p => p.EmployeeID).Property(p => p.EmployeeID).HasDatabaseGeneratedOption(DatabaseGeneratedOption.None); Property(p => p.Name).IsRequired().HasMaxLength(100); Property(p => p.Gender).IsRequired().HasMaxLength(1); Map<SalesMan>(salesman => { salesman.ToTable("SalesMan"); }); Map<SalesManager>(manager => { manager.ToTable("Manager"); }); } }
我們通過使用ToTable方法,讓Code First為每個子類型建立一個表,表的名字就是ToTable方法中傳入的參數值,子類對應的表中的主鍵與基類對應的表中的主鍵名字相同,同時它還是指向基類對應的表的外鍵。
我們還使用上面的那個測試方法來測試一下Code First按照TPT的方式建立的數據表結構。
3.Table Per Concrete Type(TPC)
在這種處理方式中,Entity Framework Code First為每一個子類建立一個表,在子類對應的表中除了子類特有的屬性外還有基類的屬性對應的表。
和TPT一樣,我們也需要通過Map方法進行設置。
public class SalesPersonValueObjectConfiguration: EntityTypeConfiguration<SalesPerson> { public SalesPersonValueObjectConfiguration() { HasKey(p => p.EmployeeID).Property(p => p.EmployeeID).HasDatabaseGeneratedOption(DatabaseGeneratedOption.None); Property(p => p.Name).IsRequired().HasMaxLength(100); Property(p => p.Gender).IsRequired().HasMaxLength(1); Map<SalesMan>(salesman => { salesman.ToTable("SalesMan"); salesman.MapInheritedProperties(); }); Map<SalesManager>(manager => { manager.ToTable("Manager"); manager.MapInheritedProperties(); }); } }
通過MapInheritedProperties方法就可以強制Code First使用TPC方式。
我們重新編譯之后執行我們原來的測試方法,可以得到不同的數據表結構,Code First不會為基類建立表,而是為每個子類都建立一個表,將子類的內容和基類的內容都存儲到各個子類對應的表中。
PS:如果你的基類是abstract,效果也是一樣的。
最后需要探討的一個問題是我們在實際項目中應該使用哪種方式呢?
1.不推薦使用TPC(Type Per Concrete Type),因為在TPC方式中子類中包含的其他類的實例或實例集合不能被映射為表之間的關系。你必須通過手動地在類中添加依賴類的主鍵屬性,從而讓Code First感知到它們之間的關系,而這種方式是和使用Code First的初衷相反的。
2.從查詢性能上來說,TPH會好一些,因為所有的數據都存在一個表中,不需要在數據查詢時使用join。
3.從存儲空間上來說,TPT會好一些,因為使用TPH時所有的列都在一個表中,而表中的記錄不可能使用所有的列,於是有很多列的值是null,浪費了很多存儲空間。
4.從數據驗證的角度來說,TPT好一些,因為TPH中很多子類屬性對應的列是可為空的,就為數據驗證增加了復雜性。
所以說具體的項目中選擇哪種方式取決於你的實際項目需要。
下一次的日記將介紹映射遺留系統的數據庫需要的一些技術。