翻譯的初衷以及為什么選擇《Entity Framework 6 Recipes》來學習,請看本系列開篇
6-6 映射派生類中的NULL條件
問題
你的表中,有一列允許為null。你想使用TPH創建一個模型,列值為null時,表示一個派生類型,不為null時,表示另一個派生類型。
解決方案
假設你有一張表,描述醫學實驗葯物。這張表包含一列指示該葯是什么時候批准生產的。葯在批准生產之前都被認為是實驗性的。一但批准生產,它就被認為是葯物了。我們就以圖6-7中Drug表開始我們這一小節的學習。

圖6-7 Drug表中有一列可為null鑒別列,AcceptedDate
按下面的步驟為Drug表建模:
1、創建一個派生至DbContext的上下文對象Recipe6Context;
2、創建POCO實體類Drug、Medicine和Experimental,如代碼清單6-17所示;
代碼清單6-17. 創建POCO實體Drug、Medicine和Experimental
1 [Table("Drug", Schema = "Chapter6")] 2 public abstract class Drug 3 { 4 [Key] 5 [DatabaseGenerated(DatabaseGeneratedOption.Identity)] 6 public int DrugId { get; set; } 7 public string Name { get; set; } 8 } 9 10 public class Experimental : Drug 11 { 12 public string PrincipalResearcher { get; set; } 13 14 public void PromoteToMedicine(DateTime acceptedDate, decimal targetPrice, 15 string marketingName) 16 { 17 var drug = new Medicine { DrugId = this.DrugId }; 18 using (var context = new Recipe6Context()) 19 { 20 context.Drugs.Attach(drug); 21 drug.AcceptedDate = acceptedDate; 22 drug.TargetPrice = targetPrice; 23 drug.Name = marketingName; 24 context.SaveChanges(); 25 } 26 } 27 28 } 29 30 public class Medicine : Drug 31 { 32 public decimal? TargetPrice { get; set; } 33 public DateTime AcceptedDate { get; set; } 34 }
3、在上下文對象Recipe6Context中添加一個類型為DbSe<Drug>的屬性;
4、在上下文對象Recipe6Context中重寫OnModelCreating方法,讓它為臨床用葯(Medicine)和試驗性葯物(Experimental)類型配置TPH映射。如代碼清單6-18所示;
代碼清單6-18.重寫OnModelCreating方法,使之配置TPH映射
1 protected override void OnModelCreating(DbModelBuilder modelBuilder) 2 { 3 base.OnModelCreating(modelBuilder); 4 modelBuilder.Entity<Experimental>() 5 .Map(m => m.Requires("AcceptedDate").HasValue((DateTime?)null)); 6 modelBuilder.Entity<Medicine>() 7 .Map(m => m.Requires(d => d.AcceptedDate).HasValue()); 8 }
原理
在示例中,我們使用null和非null作為條件分別映射,不包含AcceptedDate的試驗性葯物(Experimental)和包含AcceptedDate的臨床用葯(Medicine)。和多數繼承的例子一樣,我們創建一個抽象的基類實體,之所以為抽象,是因為在我們的模型中,不會使用一個未分類的葯物。
有趣的是,在Medicine實體中,我們將鑒別列映射到一個標題屬性上。在絕大多數情況下,將鑒別列映射到一個標量屬性是被禁止的。然而,在這個示例中,我們使用null和非null作為條件,不僅如此,AcceptedDate還設為不可空,這些對屬性值的約束使得該屬性可以被映射。
在代碼清單6-19中,我們插入了兩個試驗性葯物,並查詢出插入的數據。我們使用AcceptedDate屬性為我們提供的機會,可以將一個對象從一個派生類型改變為另一個派生類型。在我們的示例中,我們創建了兩個試驗性葯物,隨后將他們中的一個提升為臨床用葯。
代碼清單6-19.插入並獲取我們的派生類型實例
using (var context = new Recipe6Context()) { var exDrug1 = new Experimental { Name = "Nanoxol", PrincipalResearcher = "Dr. Susan James" }; var exDrug2 = new Experimental { Name = "Percosol", PrincipalResearcher = "Dr. Bill Minor" }; context.Drugs.Add(exDrug1); context.Drugs.Add(exDrug2); context.SaveChanges(); // Nanoxol剛獲生產批准 exDrug1.PromoteToMedicine(DateTime.Now, 19.99M, "Treatall"); context.Entry(exDrug1).State = EntityState.Detached; //不在使用此實例 } using (var context = new Recipe6Context()) { Console.WriteLine("Experimental Drugs"); foreach (var d in context.Drugs.OfType<Experimental>()) { Console.WriteLine("\t{0} ({1})", d.Name, d.PrincipalResearcher); } Console.WriteLine("Medicines"); foreach (var d in context.Drugs.OfType<Medicine>()) { Console.WriteLine("\t{0} Retails for {1}", d.Name, d.TargetPrice.Value.ToString("C")); } }
代碼清單6-19的輸出如下:
Experimental Drugs Percosol (Dr. Bill Minor) Medicines Treatall Retails for $19.99
我們使用PromoteToMedicine()方法將一個試驗性葯物(Experimental)提升為臨床用葯(Medicine)。在這個方法的實現中,我們創建了一個Medicine實例,將它附加到一個新的上下文中,並使用適當的值初始化它。一旦這個新的實例被初始化和附加,我們就調用SaveChanges()方法將它保存到數據庫中,因為這個實例有一個和試驗性葯物相同的鍵(DrugId),實體框架會產生一個update(更新)語句,而不是insert(插入)語句。
我們在POCO類Experimental中實現了這個方法。這讓我們可以無縫地在這樣的類中添加一個方法,通過這種方式,提供了一種更簡潔的實現 。話雖這么說,但我們感興趣的是,創建一個透明持久化(persistence-ignorant)的POCO實體,能被用在多個上下文對象中,這讓我們可以在helper類中實現一個稍微不同的版本。
6-7 使用一個非主鍵列建模TPT繼承映射
問題
在一個存在的架構中,你有一張或多張表,與一張共享表有一對一的關系,共享表中使用的鍵在表中不是主鍵,你想使用TPT對此建模。
解決方案
假設你的數據庫中包含的數據庫關系圖如圖6-8所示。

圖6-8 一個包含staff,Principal和Instructor表的關系對象圖
在圖6-8中,我們一張職工(Staff)表,它包含員工的姓名(Name),兩張包含校長(Principal)、教員(Instructor)信息的表。這里需要引起注意的是,表Principal和Instructor中的主鍵不是Staff表的外鍵。在這種類型的關系結構是不能直接使用TPT繼承映射的。對於TPT,關聯表的主鍵必須是主表(基表)的外鍵。同時,還要注意的是,關系是一對一。這是因為我們對Principal和Instructor表中StaffId列創建了唯一索引的約束。
按下面的步驟,為圖6-8中的表及其關系建模:
1、在你的項目中添加一個ADO.NET Entity Data Model(ADO.NET實體數據模型),並導入表Staff,Principal,和Instructor;
2、刪除實體Principal與Staff之間的關聯,實體Instructor與Staff之間的關聯;
3、右鍵Staff實體,選擇Add(增加) ➤Inheritance(繼承)。選擇Staff作為基類,Principal作為派生類。重復前面的操作,選擇Staff作為基類,Instructor作為派生類型。
4、從實體Instructor和Principal中刪除屬性StaffId;
5、右鍵實體Staff,選擇Properties(屬性)。設置Abstract(抽象)屬性為True。這會讓實體Staff成為一個抽象類;
6、因為StaffId在表Principal和Instructor中不是主鍵,所以我們不能使用默認表映射來映射實體Principal和Instructor,或者Staff。依次選擇每個表,並在映射詳細信息窗口刪除表映射。
7、使用代碼清單6-20中的代碼創建存儲過程,我們會將這些存儲過程映射到實體Principal和Instrucotr中的插入、更新和刪除操作;
代碼清單6-20. 實體Instructor和Principal插入、更新和刪除動作的存儲過程
1 create procedure [chapter6].[InsertInstructor] 2 (@Name varchar(50), @Salary decimal) 3 as 4 begin 5 declare @staffid int 6 insert into Chapter6.Staff(Name) values (@Name) 7 set @staffid = SCOPE_IDENTITY() 8 insert into Chapter6.Instructor(Salary,StaffId) values (@Salary,@staffid) 9 select @staffid as StaffId,SCOPE_IDENTITY() as InstructorId 10 end 11 go 12 create procedure [chapter6].[UpdateInstructor] 13 (@Name varchar(50), @Salary decimal, @StaffId int, @InstructorId int) 14 as 15 begin 16 update Chapter6.Staff set Name = @Name where StaffId = @StaffId 17 update Chapter6.Instructor set Salary = @Salary where InstructorId = @InstructorId 18 end 19 go 20 create procedure [chapter6].[DeleteInstructor] 21 (@StaffId int) 22 as 23 begin 24 delete Chapter6.Staff where StaffId = @StaffId 25 delete Chapter6.Instructor where StaffId = @StaffId 26 end 27 go 28 create procedure [Chapter6].[InsertPrincipal] 29 (@Name varchar(50),@Salary decimal,@Bonus decimal) 30 as 31 begin 32 declare @staffid int 33 insert into Chapter6.Staff(Name) values (@Name) 34 set @staffid = SCOPE_IDENTITY() 35 insert into Chapter6.Principal(Salary,Bonus,StaffId) values 36 (@Salary,@Bonus,@staffid) 37 select @staffid as StaffId, SCOPE_IDENTITY() as PrincipalId 38 end 39 go 40 create procedure [Chapter6].[UpdatePrincipal] 41 (@Name varchar(50),@Salary decimal, @Bonus decimal, @StaffId int, @PrincipalId int) 42 as 43 44 begin 45 update Chapter6.Staff set Name = @Name where StaffId = @StaffId 46 update Chapter6.Principal set Salary = @Salary, Bonus = @Bonus where 47 PrincipalId = @PrincipalId 48 end 49 go 50 create procedure [Chapter6].[DeletePrincipal] 51 (@StaffId int) 52 as 53 begin 54 delete Chapter6.Staff where StaffId = @StaffId 55 delete Chapter6.Principal where StaffId = @StaffId 56 end
8、右鍵設計器窗口,選擇Update Model from Database(從數據庫中更新模型)。添加第7步創建的存儲過程;
9、選擇實體Principal,並查看Mapping Details window(映射詳細信息窗口)。單擊Map Entity to Function(映射實體到函數)按鈕。這個按鈕是在映射詳細信息窗口左邊最下邊的一個按鈕。映射Insert、Update和Delete動作到存儲過程。確保映射插入動作的result columns(結果列)StaffId和PrincipalId(如圖6-9)。

圖6-9 為實體Principal映射Insert,Update和Delete動作
10、在上實體Instructor重復第9步。確保映射插入動作的result columns(結果列)StaffId和PrincipalId。
在解決方案瀏覽器中右鍵.edmx文件,選擇Open With(打開方式) ➤XML Editor(XML文本編輯器)。這將關閉設計器窗口並在XML編輯器中打開.edmx文件。滾動到映射怪中的標簽<EntityContainerMapping>。將代碼清單6-21中的查詢視圖(QueryView)插入到標簽<EntitySetMapping>中。
1 <EntitySetMapping Name="Staffs"> 2 <QueryView> 3 select value 4 case 5 when (i.StaffId is not null) then 6 Apress.EF6Recipes.BeyondModelingBasics.Recipe7.Instructor(s.StaffId,s.Name,i.InstructorId,i.Salary) 7 when (p.StaffId is not null) then 8 Apress.EF6Recipes.BeyondModelingBasics.Recipe7.Principal(s.StaffId,s.Name,p.PrincipalId,p.Salary,p.Bonus) 9 END 10 from ApressEF6RecipesBeyondModelingBasicsRecipe7StoreContainer.Staff as s 11 left join ApressEF6RecipesBeyondModelingBasicsRecipe7StoreContainer.Instructor as i 12 on s.StaffId = i.StaffId 13 left join ApressEF6RecipesBeyondModelingBasicsRecipe7StoreContainer.Principal as p 14 on s.StaffId = p.StaffId 15 </QueryView> 16 </EntitySetMapping>
原理
使用TPT繼承映射,實體框架要求基類表中的外鍵是派生類中的主鍵。在我們的示例中,每個派生類表有自己獨立的主鍵。
為了創建TPT繼承映射模型,在概念層,實體Principal和Instructor繼承自實體Staff。接下來,我們刪除導入表時創建的映射。然后我們使用一個QueryView表達式來創建一個新的映射。使用QueryView將Insert、Update和Delete動作放入我們的代碼中。為了處理這些動作,我們在數據庫中創建了額外的存儲過程。
我們使用QueryView將映射派生類中的標量屬性到數據庫表中。QueryView中的關鍵部分是case語句。這里有兩種情況,我們有一個Principal和一個Instructor。如果Instructor的StaffId非null時,我們就得到一個Instructor實例;如果Principal的StaffId為null時,我們就得到一個Principal實例。表達式剩下的部分是,引入派生類表中的行。
代碼清單6-22插入一位校長和一位教員到數據庫表中。
代碼清單6-22. 從模型中插入和獲取
1 using (var context = new Recipe7Context()) 2 { 3 var principal = new Principal 4 { 5 Name = "Robbie Smith", 6 Bonus = 3500M, 7 Salary = 48000M 8 }; 9 var instructor = new Instructor 10 { 11 Name = "Joan Carlson", 12 Salary = 39000M 13 }; 14 context.Staffs.Add(principal); 15 context.Staffs.Add(instructor); 16 context.SaveChanges(); 17 } 18 19 using (var context = new Recipe7Context()) 20 { 21 Console.WriteLine("Principals"); 22 Console.WriteLine("=========="); 23 foreach (var p in context.Staffs.OfType<Principal>()) 24 { 25 Console.WriteLine("\t{0}, Salary: {1:C}, Bonus: {2:C}", 26 p.Name, p.Salary, 27 p.Bonus); 28 } 29 Console.WriteLine("Instructors"); 30 Console.WriteLine("==========="); 31 foreach (var i in context.Staffs.OfType<Instructor>()) 32 { 33 Console.WriteLine("\t{0}, Salary: {1:C}", i.Name, i.Salary); 34 } 35 }
代碼清單6-22的輸出如下:
Principals ========== Robbie Smith, Salary: $48,000.00, Bonus: $3,500.00 Instructors =========== Joan Carlson, Salary: $39,000.00
實體框架交流QQ群: 458326058,歡迎有興趣的朋友加入一起交流
謝謝大家的持續關注,我的博客地址:http://www.cnblogs.com/VolcanoCloud/
