本章節講解EF里的繼承映射關系,分為TPH、TPT、TPC。具體:
1.TPH:Table Per Hierarchy
這是EF的默認的繼承映射關系:一張表存放基類和子類的所有列,自動生成的discriminator列用來區分基類和子類的數據。新建一個度假村Resort實體類試試:
/// <summary> /// 度假村類 /// </summary> public class Resort : Lodging //這里繼承了Lodging類 { public string Entertainment { get; set; } //娛樂 public string Activities { get; set; } //活動 }
之前的住宿類Lodging里有個屬性IsResort表示是否度假勝地,現在可以注釋掉了,有新的類Resort來繼承Lodging表示是否是度假勝地了,跑下程序最終會生成一張表:
並沒有生成Resorts表,而是把Resrot實體類里的屬性生成到了Lodgings表里。多了一列discriminator,這個是默認的,用來表示數據來自哪個類,繼續添加一個插入Lodging表數據的方法:
private static void InsertLodging() { var lodging = new CodeFirst.Model.Lodging { Name = "Rainy Day Motel", Destination = new CodeFirst.Model.Destination { Name = "Seattle, Washington", Country = "USA" } }; using (var context = new CodeFirst.DataAccess.BreakAwayContext()) { context.Lodgings.Add(lodging); context.SaveChanges(); } }
再添加一個插入Resort表數據的方法:
private static void InsertResort() { var resort = new CodeFirst.Model.Resort { Name = "Top Notch Resort and Spa", MilesFromNearestAirport = 30, Activities = "Spa, Hiking, Skiing, Ballooning", Destination = new CodeFirst.Model.Destination { Name = "Stowe, Vermont", Country = "USA" } }; using (var context = new CodeFirst.DataAccess.BreakAwayContext()) { context.Lodgings.Add(resort); context.SaveChanges(); } }
在Main方法里調用兩個插入方法,可得到如下數據:
兩個插入的數據都到了一張表里。Discriminator列表示數據來自哪一列。當然是可以配置的,這里就必須使用Fluent API配置了,Data Annotation表示無能為力,到LodgingMap里進行配置:
this.Map<CodeFirst.Model.Lodging>(l => { l.Requires("From").HasValue("Standard"); }); this.Map<CodeFirst.Model.Resort>(l => { l.Requires("From").HasValue("Resort"); });
生成了我們指定的From列,數據Standard、Resort分別表示來自Lodging和Resrot表,形象點就是1號酒店是普通酒店,2號就是是度假勝地的酒店:
當然,這里甚至可以把HasValue方法里的參數設置成True和False,用布爾類型的數據區分普通酒店和度假勝地的酒店更形象,園友lk8167給了一個更形象的普通售貨員和銷售經理的例子
2.TPT:Table Per Type
父類和子類在不同的表里。使用Data Annotation配置TPT:
[Table("Resorts")] public class Resort : Lodging { public string Entertainment { get; set; } public string Activities { get; set; } }
或者使用Fluent API配置:
this.Map(m => { m.ToTable("Lodgings"); }).Map<CodeFirst.Model.Resort>(m => { m.ToTable("Resorts"); });
注意:上面配置TPH的Fluetn API需要注釋掉在跑程序,那是測試TPH的配置。同時釋放這句的注釋:context.Database.Initialize(true);,這里沒修改實體,但是也需要重新生成數據庫。最終數據庫是這樣的:
父類和子類實體都有一張表,子表通過主鍵LodgingId找到父表:
3.TPC:Table Per Concrete Type
為每個子類建立一個表,每個與子類對應的表中包含基類的屬性對應的列和子類特有屬性對應的列。同樣之前配置TPT的Fluent API需要先注釋掉,然后我們通過Fluent API配置下TPC,TPC也無法用Data Annotation配置:
this.Map(m => { m.ToTable("Lodgings"); }).Map<CodeFirst.Model.Resort>(m => { m.ToTable("Resorts"); m.MapInheritedProperties(); });
生成的數據庫:
可見,子類Resorts類也有了基類的所有屬性。
注意:為了方便測試生成TPC,我注釋了所有Lodging表的導航屬性,主要是和Destination的一對多關系、Destination類也需要注釋掉Lodging屬性和Fluent API關系配置,否則程序跑起來會報DataException錯:
An exception occurred while initializing the database. See the InnerException for details.
大家下載demo使用的時候也需要先注釋掉Lodging類的導航屬性。當然不注釋想保留也可以,必須設置外鍵為可空類型,具體請參考Programming Entity Framework: Code First 第五章 Avoiding Mapping Exceptions with TPC
講了這幾種方式配置繼承映射,實際項目中應該用哪個呢?
- 不推薦使用TPC(Type Per Concrete Type),因為在TPC方式中子類中包含的其他類的實例或實例集合不能被映射為表之間的關系。你必須通過手動地在類中添加依賴類的主鍵屬性,從而讓 Code First感知到它們之間的關系,而這種方式是和使用Code First的初衷相反的;
- 從查詢性能上來說,TPH會好一些,因為所有的數據都存在一個表中,不需要在數據查詢時使用join;
- 從存儲空間上來說,TPT會好一些,因為使用TPH時所有的列都在一個表中,而表中的記錄不可能使用所有的列,於是有很多列的值是null,浪費了很多存儲空間;
- 從數據驗證的角度來說,TPT好一些,因為TPH中很多子類屬性對應的列是可為空的,就為數據驗證增加了復雜性。
本系列文章結束,主要講解了EF里如何使用Code First的方式配置數據庫,基本上都是手寫的配置,其實大家可能已經想到會有工具可以自動配置這些關系了,對了,就是EF Power Tools。這個工具相當智能,可以直接配置出所有的關系。不過個人還是建議關系不多的話自己手寫Fluent API來配置。
另外,前面配置了那么長時間的一對多、多對多等各種關系。配置好了如何用EF對這些數據進行增查改查呢?后續還會有系列文章講解EF是如何操作數據庫的,請保持關注。
EF Code First 系列文章導航: