《Entity Framework 6 Recipes》中文翻译系列 (35) ------ 第六章 继承与建模高级应用之TPH继承映射中使用复合条件

时间:2022-07-17 04:24:36

翻译的初衷以及为什么选择《Entity Framework 6 Recipes》来学习,请看本系列开篇

6-11  TPH继承映射中使用复合条件

问题

  你想使用TPH为一张表建模,建模中使用的复杂条件超过了实框架能直接支持的能力。

解决方案

  假设我们有一张Member表,如图6-15所示。Member表描述了我们俱乐部的会员信息。在我们的模型中,我们想使用TPH为派生类,AdultMember(成人会员)、SeniorMember(老年人会员)和TeenMember(青少年会员)建模。

《Entity Framework 6 Recipes》中文翻译系列 (35) ------ 第六章 继承与建模高级应用之TPH继承映射中使用复合条件

图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动作使用的存储过程

 create procedure [Chapter6].[InsertMember]
(@Name varchar(50), @Phone varchar(50), @Age int)
as
begin
insert into Chapter6.Member (Name, Phone, Age)
values (@Name,@Phone,@Age)
select SCOPE_IDENTITY() as MemberId
end create procedure [Chapter6].[UpdateMember]
(@Name varchar(50), @Phone varchar(50), @Age int, @MemberId int)
as
begin
update Chapter6.Member set Name=@Name, Phone=@Phone, Age=@Age
where MemberId = @MemberId
end create procedure [Chapter6].[DeleteMember]
(@MemberId int)
as
begin
delete from Chapter6.Member where MemberId = @MemberId
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所示。

《Entity Framework 6 Recipes》中文翻译系列 (35) ------ 第六章 继承与建模高级应用之TPH继承映射中使用复合条件

图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

   <EntitySetMapping Name="Members">
<QueryView>
select value
case
when m.Age &lt; 20 then
Apress.EF6Recipes.BeyondModelingBasics.Recipe11.Teen(m.MemberId,m.Name,m.Phone,m.Age)
when m.Age between 20 and 55 then
Apress.EF6Recipes.BeyondModelingBasics.Recipe11.Adult(m.MemberId,m.Name,m.Phone,m.Age)
when m.Age > 55 then
Apress.EF6Recipes.BeyondModelingBasics.Recipe11.Senior(m.MemberId,m.Name,m.Phone,m.Age)
end
from ApressEF6RecipesBeyondModelingBasicsRecipe11StoreContainer.Member as m
</QueryView>
</EntitySetMapping>

最终的模型如图6-17所示。

《Entity Framework 6 Recipes》中文翻译系列 (35) ------ 第六章 继承与建模高级应用之TPH继承映射中使用复合条件

图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.从模型中插入和获取数据

 using (var context = new Recipe11Context())
{
var teen = new Teen
{
Name = "Steven Keller",
Age = 17,
Phone = "817 867-5309"
};
var adult = new Adult
{
Name = "Margret Jones",
Age = 53,
Phone = "913 294-6059"
};
var senior = new Senior
{
Name = "Roland Park",
Age = 71,
Phone = "816 353-4458"
};
context.Members.Add(teen);
context.Members.Add(adult);
context.Members.Add(senior);
context.SaveChanges();
} using (var context = new Recipe11Context())
{
Console.WriteLine("Club Members");
Console.WriteLine("============");
foreach (var member in context.Members)
{
bool printPhone = true;
string str = string.Empty;
if (member is Teen)
{
str = " a Teen";
printPhone = false;
}
else if (member is Adult)
str = "an Adult";
else if (member is Senior)
str = "a Senior";
Console.WriteLine("{0} is {1} member, phone: {2}", member.Name,
str, printPhone ? member.Phone : "unavailable");
}
}

代码清单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事件中处理验证

  public partial class Recipe11Context
{
public override int SaveChanges()
{
//译注:书使用的是下面注释的这句,因为这是原书的第二版,
//书中的代码很可能是第一版时的,没被更新而遗留下来的。
//this.SavingChanges += new EventHandler(Validate)
Validate();
return base.SaveChanges();
} public void Validate()
{
var entities = ((IObjectContextAdapter)this).ObjectContext.ObjectStateManager
.GetObjectStateEntries(EntityState.Added |
EntityState.Modified)
.Select(et => et.Entity as Member);
foreach (var member in entities)
{
if (member is Teen && member.Age > )
{
throw new ApplicationException("Entity validation failed");
}
else if (member is Adult && (member.Age < || member.Age >= ))
{
throw new ApplicationException("Entity validation failed");
}
else if (member is Senior && member.Age < )
{
throw new ApplicationException("Entity validation failed");
}
}
} }

  在代码清单6-34中,当调用SaveChanges()方法时,我们的Validate()方法将检查每个新增或修改的对象。对于每一个实体的实例,我们验证它的属性Age是否和它的类型对应。当发现一个验证错误时,我们简单地抛出一个异常。

  在第十二章,我们将用几个小节来介绍,更改被提交到数据库前的事件处理和验证。

实体框架交流QQ群:  458326058,欢迎有兴趣的朋友加入一起交流

谢谢大家的持续关注,我的博客地址:http://www.cnblogs.com/VolcanoCloud/