继续讨论EF中使用存储过程的问题,这回着重讨论的是为存储过程的参数进行赋值的问题。说得更加具体一点,是如何为实体映射的Delete存储过程参数进行赋值的问题。关于文中涉及的这个问题,我个人觉得是EF一个有待改进的地方,不知道各位看官是否同意?
目录
一、EF存储过程参数赋值的版本策略
二、Delete存储参数就一定是Original值吗?
三、如果直接修改.edmx模型的XML呢?
四、为Delete存储过程参数赋上Current值,如何做得到?
一、EF存储过程参数赋值的版本策略
和传统的基于DataSet的ADO.NET类似,EF的核心功能之一就是“状态追踪(State Tacking)”。这中间实际上又涉及到两个方面:通过状态决定数据更新的类型(Insert、Update和Delete);以及同时保存不同版本的属性值(Current值和Original值)。版本策略主要是针对Update操作设计的,一般来讲组成Where条件的为Original值,而更新的值为Current值。
正是因为只有Update操作才需要显式指定映射的是实体属性值的版本(Current/Original),所以在进行实体/存储过程映射的时候,只有Update存储过程才可以选择“是否采用原始值(Use Original Value)”。Insert和Delete存储过程默认的版本为Current和Original。反映在VS的.edmx模型设计器上就是:只有Update存储过程的参数映射才具有“Use Original Value”这个复选框。
二、Delete存储参数队应的就一定是Original值吗?
粗略地想想,EF这样设计也无可厚非:Insert存储过程用于添加一条全新的记录,自然应该采用当前值;而Delete存储过程用于删除一条现有的记录,删除操作的筛选条件自然应该使用原始值。但是,我们忽略掉一点:Delete存储过程一定非得执行删除操作吗?如果我进行“逻辑删除”,实际上进行的是Update操作。关于逻辑删除的实现,可以参阅我上一篇文章《逻辑删除的实现与自增长列值返回》。
如果你看了我提到的这篇文章,你可能会问,即使在文中介绍的关于“逻辑删除”的场景中,也没有使用当前值得要求呀。是的,上一篇文章提到的逻辑删除确实也只需要传入实体属性的原始值作为Delete存储过程的参数,现在我们就举一个这样的例子。
通过是使用T_CONTACT这张简单不过的表,同样是采用逻辑删除。不过现在有这样的一个要求,对于条存储在的记录,我们需要记录最后修改者是谁。对于一条被逻辑删除掉的记录,这个最后修改者就是删除掉该条记录的人。这是一个很常见的需求,为此我们可以直接在T_CONTACT的数据表中添加一个新的字段:LAST_UPDATED_BY,创建该表的DDL定义如下:
1: CREATE TABLE [T_CONTACT]
2: (
3: [ID] [INT] IDENTITY(1,1) PRIMARY KEY,
4: [NAME] [NVARCHAR](50) NOT NULL,
5: [IS_DELETED] [BIT] NOT NULL,
6: [LAST_UPDATED_BY] [NVARCHAR](50) NOT NULL
7: )
那么对于Delete存储过程,除了指定需要删除的记录的主键之外,还需要将当前用户名作为参数作为传进来。这样的一个存储过程具有如下的定义
1: CREATE PROCEDURE [dbo].[P_CONTACT_D]
2: (
3: @p_id INT,
4: @user_name NVARCHAR(50)
5: )
6: AS
7: BEGIN
8: UPDATE T_CONTACT
9: SET IS_DELETED = 1,
10: LAST_UPDATED_BY = @user_name
11: WHERE ID = @p_id
12: END
在实际操作场景下,我们需要先获取一条现有的Contact记录,然后将其标记为删除。然后Delete存储过程被执行,并且采用预先定义好的实体属性/参数的映射关系来对存储过程的参数进行赋值。但是,由于Delete存储过程默认使用的是实体对象的初始值,即使你在删除之前为Contact对象的LastUpdatedBy属性设置了新的值,该值也不可能传入到存储过程中去。
三、如果直接修改.edmx模型的XML呢?
由于Delete过程只能接受实体的映射属性的初始值作为参数,导致我们无法指定一个新的值作为参数。我想有人会有这样的疑问:VS提供的设计器不能提供你指定Delete存储过程参数版本的功能,你是否可以直接修改.edmx文件的XML呢?我们不妨来尝试一下:
在整个XML中,实体的CUD存储过程映射对应如下一段XML片段,我们可以看到,只有UpdateFunction中的参数映射节点才有Version属性(而且这是一个必需的属性),用于指定参数定义的是Original值还是Current值。
1: <EntityTypeMapping TypeName="EFExtensionsModel.Contact">
2: <ModificationFunctionMapping>
3: <InsertFunction FunctionName="EFExtensionsModel.Store.P_CONTACT_I" >
4: <ScalarProperty Name="LastUpdatedBy" ParameterName="user_name" />
5: <ScalarProperty Name="Name" ParameterName="p_name" />
6: <ResultBinding Name="ID" ColumnName="ID" />
7: </InsertFunction>
8: <UpdateFunction FunctionName="EFExtensionsModel.Store.P_CONTACT_U" >
9: <ScalarProperty Name="LastUpdatedBy" ParameterName="user_name" Version="Current" />
10: <ScalarProperty Name="Name" ParameterName="p_name" Version="Current" />
11: <ScalarProperty Name="ID" ParameterName="p_id" Version="Original" />
12: </UpdateFunction>
13: <DeleteFunction FunctionName="EFExtensionsModel.Store.P_CONTACT_D" >
14: <ScalarProperty Name="LastUpdatedBy" ParameterName="user_name" />
15: <ScalarProperty Name="ID" ParameterName="p_id" />
16: </DeleteFunction>
17: </ModificationFunctionMapping>
18: </EntityTypeMapping>
那些现在我们将DeleteFunction的user_name参数的映射节点人为地加上Version=“Current”属性设置。
1: <DeleteFunction FunctionName="EFExtensionsModel.Store.P_CONTACT_D" >
2: <ScalarProperty Name="LastUpdatedBy" ParameterName="user_name" Version="Current" />
3: <ScalarProperty Name="ID" ParameterName="p_id" />
4: </DeleteFunction>
但是当你进行编译的时候,会出现如下的错误,明确告诉你:“This function mapping can only contain bindings to 'original' property versions.”
四、为Delete存储过程参数赋上Current值,如何做得到?
从上面的介绍我们不难发现,Delete存储过程不能接受基于当前值得参数映射,并不仅仅是设计器不支持,EF本来就是这样设计的。在这种情况下要实现我们的要求,只有一个办法:将当前值转化成初始值值,这样的转变通过调用ObjectContext的AcceptAllChanges方法可以实现。具体来说,对于需要删除的实体,现设定LastUpdatedBy属性,然后调用AcceptAllChanges方法,然后再调用ObjectStateManager的ChangeObjectState方法将状态设置为Deleted。最终通过调用SaveChanges方法提交更新,具体的代码如下:
1: static void Main(string[] args)
2: {
3: using (EFExtensionsEntities context = new EFExtensionsEntities())
4: {
5: Contact contact = new Contact { Name = "Foo", LastUpdatedBy = "Bar" };
6: context.Contacts.AddObject(contact);
7: context.SaveChanges();
8: Console.WriteLine("{0}: {1}", contact.ID, contact.Name);
9:
10: contact.LastUpdatedBy = "Baz";
11: context.AcceptAllChanges();
12: context.ObjectStateManager.ChangeObjectState(contact, EntityState.Deleted);
13: context.SaveChanges();
14: }
15: }
执行上面的程序后,你会在数据库中发现为删除对象指定的LastUpdatedBy属性“Baz”,而不是初始值“Bar”最终反映在数据库中。
虽然通过“曲线救国”我们可以实现为实体映射的Delete存储过程指定一个“新值”作为某个参数的值,但是这样的做法总觉得不怎么优雅。所以,我个人觉得这是EF一个值得改进的地方,让Delete存储过程和Update一样,也可以指定不同的版本。