《Entity Framework 6 Recipes》中文翻译系列 (32) ------ 第六章 继承与建模高级应用之TPH与TPT (1)
翻译的初衷以及为什么选择《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/