第六章 继承与建模高级应用之TPH继承映射中使用复合条件

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

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所示。

图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所示。

图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 > 19)
            {
                throw new ApplicationException("Entity validation failed");
            }
            else if (member is Adult && (member.Age < 20 || member.Age >= 55))
            {
                throw new ApplicationException("Entity validation failed");
            }
            else if (member is Senior && member.Age < 55)
            {
                throw new ApplicationException("Entity validation failed");
            }
        }
    }

}

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

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

《Entity Framework 6 Recipes》中文翻译系列