《Entity Framework 6 Recipes》中文翻译系列 (35) ------ 第六章 继承与建模高级应用之TPH继承映射中使用复合条件
翻译的初衷以及为什么选择《Entity Framework 6 Recipes》来学习,请看本系列开篇
6-11 TPH继承映射中使用复合条件
问题
你想使用TPH为一张表建模,建模中使用的复杂条件超过了实框架能直接支持的能力。
解决方案
假设我们有一张Member表,如图6-15所示。Member表描述了我们俱乐部的会员信息。在我们的模型中,我们想使用TPH为派生类,AdultMember(成人会员)、SeniorMember(老年人会员)和TeenMember(青少年会员)建模。
图6-15 描述俱乐部会员的Member表
实体框架支持TPH继承映射的条件有=、is null和is not null。像<、between、和>这样简单的表达式都不能支持。我们现在的情况是, 一个会员的年龄少于20就是一位青少年会员(我们俱乐部会员最小的年龄为13)。一个会员的年龄在20到55之间就是成年会员。正如你预料的那样,如果年龄超过55就是一位老年会员。按下面的步骤为Member表和三个派生类型建模:
1、在你的项目中添加一个ADO.NET Entity Data Model(ADO.NET实体数据模型),并导入表Member;
2、右键实体Member,选择Properties(属性)。设置Abstract(抽象)属性为True。这会让实体Member成为一个抽象类;
3、使用代码清单6-31中的代码创建存储过程。我们将使用它处理继承至Member实体的insert,update和delete动作;
代码清单6-31. Insert,Update和Delete动作使用的存储过程
1 create procedure [Chapter6].[InsertMember] 2 (@Name varchar(50), @Phone varchar(50), @Age int) 3 as 4 begin 5 insert into Chapter6.Member (Name, Phone, Age) 6 values (@Name,@Phone,@Age) 7 select SCOPE_IDENTITY() as MemberId 8 end 9 10 create procedure [Chapter6].[UpdateMember] 11 (@Name varchar(50), @Phone varchar(50), @Age int, @MemberId int) 12 as 13 begin 14 update Chapter6.Member set Name=@Name, Phone=@Phone, Age=@Age 15 where MemberId = @MemberId 16 end 17 18 19 create procedure [Chapter6].[DeleteMember] 20 (@MemberId int) 21 as 22 begin 23 delete from Chapter6.Member where MemberId = @MemberId 24 end
4、右键设计器,并选择Update Model from Database(从数据库更新模型)。在更新向导中,选择上一步创建的三个存储过程;
5、右键设计器,并选择Add(新增)➤Entity(实体)。命名新实体为Teen,并设置它的基类为Member。重复这一步,创建另外两个派生类型Adult和Senior;
6、选择Member实体,并查看Mapping Details window(映射详细信息窗口).单击映射到Member,并选择<Delete>,这会删除Member表的映射;
7、选择Teen实体,并查看Mapping Details window(映射详细信息窗口)。单击Map Entity to Function(映射实体到函数)按钮。这个按钮是在映射详细信息窗口左边最下边的一个按钮。映射Insert、Update和Delete动作到存储过程。prperty/parameter(属性/参数)映射会自动填入。然而,存储过程InsertMember的返回值必须映射到MemberId属性。这是实体框架用于在插入操作后获取标识列MemberId值的途径。正确映射如图6-16所示。
图6-16 为Teen实体映射插入、更新和删除动作
8、为实体Adult和Senior重复上一步操作;
在解决方案浏览器中右键.edmx文件,选择Open With(打开方式) ➤XML Editor(XML文本编辑器),这将会在XML文本编辑器中打开.edmx文件。
9、在C-S映射一节,将代码清单6-32中的EntitySetMapping插入到标签<EntityContainerMapping>中。
代码清单6-32.映射Member表到派生类型Teen、Adult和Senior的QueryView
1 <EntitySetMapping Name="Members"> 2 <QueryView> 3 select value 4 case 5 when m.Age < 20 then 6 Apress.EF6Recipes.BeyondModelingBasics.Recipe11.Teen(m.MemberId,m.Name,m.Phone,m.Age) 7 when m.Age between 20 and 55 then 8 Apress.EF6Recipes.BeyondModelingBasics.Recipe11.Adult(m.MemberId,m.Name,m.Phone,m.Age) 9 when m.Age > 55 then 10 Apress.EF6Recipes.BeyondModelingBasics.Recipe11.Senior(m.MemberId,m.Name,m.Phone,m.Age) 11 end 12 from ApressEF6RecipesBeyondModelingBasicsRecipe11StoreContainer.Member as m 13 </QueryView> 14 </EntitySetMapping>
最终的模型如图6-17所示。
图6-17. Member和他的派生类型Senior,Adult和Teen的最终模型
原理
当使用TPH继承映射建模时,实体框架只支持有限的条件集。在本节中,我们通过QueryView定义了Member表和派生类型:Senior,Adult,和Teen的映射,扩展了实体框架支持的条件。如代码清单6-32所示。
不幸的是,使用QueryView也会付出代价。因为我得自己定义映射,还有担起了实现派生类型插入、更新和删除动作的责任。这在我们的示例中还不算困难。
在代码清单6-31中,我们定义了存储过程来处理插入、删除和更新。我们只需要创建一个实现集,这是因为这些动作的目标均是底层的Member表。在本节中,我们在数据库中使用存储过程来实现这些动作。我们还可以在.edmx文件中实现。
在设计器中,我们将存储过程映射到每个派生类型的Insert,Update和Delete动作上。这完成了在使用QueryView时需要的额外工作。
代码清单6-33演示了,从模型中插入和获取数据。在这里,我们为每个派生类型插入一个实例。在获取时,我们一起打印了会员的电话号码,青少年会员除外。
代码清单6-33.从模型中插入和获取数据
1 using (var context = new Recipe11Context()) 2 { 3 var teen = new Teen 4 { 5 Name = "Steven Keller", 6 Age = 17, 7 Phone = "817 867-5309" 8 }; 9 var adult = new Adult 10 { 11 Name = "Margret Jones", 12 Age = 53, 13 Phone = "913 294-6059" 14 }; 15 var senior = new Senior 16 { 17 Name = "Roland Park", 18 Age = 71, 19 Phone = "816 353-4458" 20 }; 21 context.Members.Add(teen); 22 context.Members.Add(adult); 23 context.Members.Add(senior); 24 context.SaveChanges(); 25 } 26 27 using (var context = new Recipe11Context()) 28 { 29 Console.WriteLine("Club Members"); 30 Console.WriteLine("============"); 31 foreach (var member in context.Members) 32 { 33 bool printPhone = true; 34 string str = string.Empty; 35 if (member is Teen) 36 { 37 str = " a Teen"; 38 printPhone = false; 39 } 40 else if (member is Adult) 41 str = "an Adult"; 42 else if (member is Senior) 43 str = "a Senior"; 44 Console.WriteLine("{0} is {1} member, phone: {2}", member.Name, 45 str, printPhone ? member.Phone : "unavailable"); 46 } 47 }
代码清单6-33的输出如下:
Members of our club
===================
Steven Keller is a Teen member, phone: unavailable
Margret Jones is an Adult member, phone: 913 294-6059
Roland Park is a Senior member, phone: 816 353-4458
这里需要注意的是,没有设计时,或者运行时检查,来验证派生类型的年龄。这样的话,完全有可能创建一个Teen的实例,设置他的年龄为74---很明显,这不是一个青少年会员。在查询时,它却被实现化为一个老年会员---这可能会得罪我们的青少年会员 。
我们能在修改被提交到数据库前引入验证。为了实现这个操作,我们在创建上下对象时注册SavingChanges事件。并在这个事件中执行验证。如代码清单6-34所示。
代码清单6-34.在SavingChanges事件中处理验证
1 public partial class Recipe11Context 2 { 3 public override int SaveChanges() 4 { 5 //译注:书使用的是下面注释的这句,因为这是原书的第二版, 6 //书中的代码很可能是第一版时的,没被更新而遗留下来的。 7 //this.SavingChanges += new EventHandler(Validate) 8 Validate(); 9 return base.SaveChanges(); 10 } 11 12 public void Validate() 13 { 14 var entities = ((IObjectContextAdapter)this).ObjectContext.ObjectStateManager 15 .GetObjectStateEntries(EntityState.Added | 16 EntityState.Modified) 17 .Select(et => et.Entity as Member); 18 foreach (var member in entities) 19 { 20 if (member is Teen && member.Age > 19) 21 { 22 throw new ApplicationException("Entity validation failed"); 23 } 24 else if (member is Adult && (member.Age < 20 || member.Age >= 55)) 25 { 26 throw new ApplicationException("Entity validation failed"); 27 } 28 else if (member is Senior && member.Age < 55) 29 { 30 throw new ApplicationException("Entity validation failed"); 31 } 32 } 33 } 34 35 }
在代码清单6-34中,当调用SaveChanges()方法时,我们的Validate()方法将检查每个新增或修改的对象。对于每一个实体的实例,我们验证它的属性Age是否和它的类型对应。当发现一个验证错误时,我们简单地抛出一个异常。
在第十二章,我们将用几个小节来介绍,更改被提交到数据库前的事件处理和验证。
实体框架交流QQ群: 458326058,欢迎有兴趣的朋友加入一起交流
谢谢大家的持续关注,我的博客地址:http://www.cnblogs.com/VolcanoCloud/