在Entity Framework中使用存储过程(一):实现存储过程的自动映射
之前给自己放了一个比较长的假期,在这期间基本上没怎么来园子逛。很多朋友的留言也没有一一回复,在这里先向大家道个歉。最近一段时间的工作任务是如何将ADO.NET Entity Framework 4.0(以下简称EF)引入到我们的开发框架,进行相应的封装、扩展,使之成为一个符合在特定场景下进行企业级快速开发的ORM。在此过程中遇到了一些挑战,也有一些心得。为了向大家分享这些心得,也为了借助大家的脑袋解决我们遇到的问题,接下来我会写一系列相关的文章。这些文章的读者适合那些对EF有基本了解的人。
第一个主题是关于在EF中使用存储过程的问题。我们知道EF不仅仅支持将一个存储过程(或者用户定义函数)转变成方法,也可以为每一个实体的映射三个Function(ADO.NET Entity Framework的术语,将存储过程和用户自定义函数统称为Function):InsertFunction、UpdateFunction和DeleteFunction,分别用于执行添加、修改和删除操作。虽然通过VS提供的设计器,我们很容易实现存储过程的导入和映射。但是,如果模型中实体和实体属性(数据表中的列)过多,这是一项很繁琐并且容易出错的工作。这篇文章就是如何避免这种烦琐的操作,实现存储过程映射的自动化。[Source Code从这里下载]
目录
一、使用存储过程的必要性
二、实现存储过程自动匹配的必要条件
三、通过T4生成新的.edmx模型
四、看看生成出来的.emdx
五、局限性
一、使用存储过程的必要性
我们知道EF通过元数据,即概念模型(Concept Model)、存储模型(Storage Model)和概念/存储映射(C/S Mapping),和状态追踪(State Tracking)机制可以为基于模型的操作自动生成SQL。对于一些简单的项目开发,这是非常理想的,因为他们完全可以不用关注数据存储层面的东西,你可以采用一些完全不具有数据库知识的开发者。但是理想总归是理想,对于企业级开发来说,我们需要的是对数据库层面数据的操作有自己的控制。在这方面,我们可以随便举两个典型的场景:
- 逻辑删除:对于一些重要的数据,我们可能需要让它们永久保存。当我们试图“删除”这些数据的时候,我们并不是将它们从数据表中移除(物理删除),而是为这条记录作一个已经被删除的标记;
- 并发处理:为了解决相同的数据在获取和提交这段时间内被另一个用户修改或者删除,我们往往SQL层面增加并发控制的逻辑。比较典型的做法是在每一个表中添加一个VersionNo这样的字段,你可以采用TimeStamp,也可以直接采用INT或者GUID。在执行Update或者Delete的SQL中判断之前获取的VersionNo是否和当前的一致。
让解决这些问题,就不能使用EF为我们自动生成的SQL,只有通过使用我们自定义的存储过程。
二、实现存储过程自动匹配的必要条件
本篇文章提供的存储过程自动映射机制是通过代码生成的方式完成的。说白了,就是读取原来的.edmx模型文件,通过分析在存储模型中使用的数据表,导入基于该表的CUD存储过程;然后再概念/存储映射节点中添加实体和这些存储过程的映射关系。那实现这样的代码生成,需要具有如下三个固定的映射规则。
- 数据表名-存储过程名:这个映射关系帮助我们通过存储模型中的实体名找到对应CUD三个存储过程(如果实体是数据表);
- 数据表列名-存储过程参数名:当存储过程被执行的时候,通过这个映射让概念模型实体某个属性值作为对应的参数;
- 存储过程参数名-版本:当进行参数赋值的时候,通过这个映射决定是使用Original或者Current版本。
在实际的开发过程中,这样的标准存储过程一般都是通过代码生成器生成的(在我的文章《创建代码生成器可以很简单:如何通过T4模板生成代码?[下篇]》中有过相应的实现),它们具有这样的映射关系。
基于这三种映射关系,我定义了如下一个名为IProcedureNameConverter的接口。其中OperationKind是我自定义的一个表示CUD操作类型的枚举。
1: public interface IProcedureNameConverter
2: {
3: string GetProcedureName(string tableName, OperationKind operationKind);
4: string GetColumnName(string parameterName);
5: DataRowVersion GetVersion(string parameterName);
6: }
7:
8: public enum OperationKind
9: {
10: Insert,
11: Update,
12: Delete
13: }
按照我们当前项目采用的命名规范,我定义了如下一个默认的DefaultNameConverter。它体现的是这样的映射关系,比如有个数据表明为T_USER(大写,单词之间用“_”隔开,并以T_为前缀),它对应的CUD存储过程名分别为:P_USER_I、P_USER_U和P_USER_D(大写,以代表存储过程的P_为前缀,后缀_I/U/D表示CUD操作类型,中间为去除前缀的表名)。如果列名为USER_ID,参数名为p_user_id(小写,加p_前缀)。如果需要用Original值为参数赋值,需要将p_前缀改成o_前缀(o_user_id)。
1: public class DefaultNameConverter: IProcedureNameConverter
2: {
3: public string GetProcedureName(string tableName, OperationKind operationKind)
4: {
5: switch (operationKind)
6: {
7: case OperationKind.Insert:
8: return string.Format("P_{0}_I", tableName.Substring(2));
9: case OperationKind.Update:
10: return string.Format("P_{0}_U", tableName.Substring(2));
11: default:
12: return string.Format("P_{0}_D", tableName.Substring(2));
13: }
14: }
15:
16: public string GetColumnName(string parameterName)
17: {
18: return parameterName.Substring(2).ToUpper();
19: }
20:
21: public DataRowVersion GetVersion(string parameterName)
22: {
23: if(parameterName.StartsWith("o"))
24: {
25: return DataRowVersion.Original;
26: }
27: else
28: {
29: return DataRowVersion.Current;
30: }
31: }
32: }
三、通过T4生成新的.edmx模型
我们采用的基于T4的代码生成,了解EF的应该对T4不会感到陌生了。如果对代码生成感兴趣的话,可以看看我的文章《与VS集成的若干种代码生成解决方案[博文汇总(共8篇)]》。这里利用借助于T4 ToolBox这个开源工具箱,并采用SQL Server SMO获取存储过程信息。所有涉及到的文本转化都实现在如下一个ProcedureMappingTemplate类型中,由于内容较多,具体实现就忽略了,有兴趣的朋友可能下载源代码。ProcedureMappingTemplate具有两个构造函数的参数分别表示:源.edmx文件,服务器和数据库名,存储过程的Schema(默认为dbo)和具体的ProcedureNameConverter(默认为DefaultNameConverter)。
1: public class ProcedureMappingTemplate: Template
2: {
3: public XmlDocument SourceModel { get; private set; }
4: public IProcedureNameConverter ProcedureNameConverter { get; private set; }
5: public Database Database { get; private set; }
6: public string Schema { get; private set; }
7:
8: public ProcedureMappingTemplate(string sourceModelFile, string serverName, string databaseName);
9: public ProcedureMappingTemplate(string sourceModelFile, string serverName, string databaseName,
10: IProcedureNameConverter procedureNameConverter, string schema);
11:
12: protected virtual XElement GenerateStorageModelNode();
13: protected virtual XElement GenerateMappingNode();
14: public override string TransformText()
15: {
16: XElement newStorageModelNode = this.GenerateStorageModelNode();
17: XElement newMappingNode = this.GenerateMappingNode();
18:
19: XmlNode storageModelNode = this.SourceModel.GetElementsByTagName("edmx:StorageModels")[0];
20: storageModelNode.InnerXml = newStorageModelNode.Elements().ToArray()[0].ToString();
21:
22: XmlNode mappingNode = this.SourceModel.GetElementsByTagName("edmx:Mappings")[0];
23: mappingNode.InnerXml = newMappingNode.Elements().ToArray()[0].ToString();
24:
25: this.WriteLine("<?xml version=\"1.0\" encoding=\"utf-8\"?>");
26: this.Write(this.SourceModel.DocumentElement.OuterXml.Replace("xmlns=\"\"",""));
27: return GenerationEnvironment.ToString();
28: }
29: }
在使用过程中,你只需要在tt模板中创建这个ProcedureMappingTemplate对象,调用Render方法即可。
1: <#@ template debug="true" hostSpecific="true" #>
2: <#@ output extension=".edmx" #>
3: <#@ assembly name="Microsoft.SqlServer.ConnectionInfo" #>
4: <#@ assembly name="Microsoft.SqlServer.Smo" #>
5: <#@ assembly name="Microsoft.SqlServer.Management.Sdk.Sfc" #>
6: <#@ assembly name="$(TargetDir)Artech.ProcedureMapping.dll" #>
7: <#@ import namespace="Artech.ProcedureMapping" #>
8: <#@ include file="T4Toolbox.tt" #>
9: <#
10: new ProcedureMappingTemplate(this.Host.ResolvePath("UserModel.edmx"),".","EFExtensions").Render();
11: #>
四、看看生成出来的.emdx
通过上面创建的TT模板(你指定的数据库中一定要存在具有相应映射关系的存储过程),新的.edmx模型文件会作为该tt文件的依赖文件被生成出来。而这个新生成的.edmx具有存储过程映射信息。具体来说,下面是原始的.edmx文件(只保留元数据节点)。
1: <?xml version="1.0" encoding="utf-8"?>
2: <edmx:Edmx Version="2.0" xmlns:edmx="http://schemas.microsoft.com/ado/2008/10/edmx">
3: <!-- EF Runtime content -->
4: <edmx:Runtime>
5: <!-- SSDL content -->
6: <edmx:StorageModels>
7: <Schema Namespace="Artech.UserModel.Store" Alias="Self" Provider="System.Data.SqlClient" ProviderManifestToken="2008" xmlns:store="http://schemas.microsoft.com/ado/2007/12/edm/EntityStoreSchemaGenerator" xmlns="http://schemas.microsoft.com/ado/2009/02/edm/ssdl">
8: <EntityContainer Name="ArtechUserModelStoreContainer">
9: <EntitySet Name="T_USER" EntityType="Artech.UserModel.Store.T_USER" store:Type="Tables" Schema="dbo" />
10: </EntityContainer>
11: <EntityType Name="T_USER">
12: <Key>
13: <PropertyRef Name="USER_ID" />
14: </Key>
15: <Property Name="USER_ID" Type="varchar" Nullable="false" MaxLength="50" />
16: <Property Name="USER_NAME" Type="nvarchar" Nullable="false" MaxLength="50" />
17: </EntityType>
18: </Schema>
19: </edmx:StorageModels>
20: <!-- CSDL content -->
21: <edmx:ConceptualModels>
22: <Schema Namespace="Artech.UserModel" Alias="Self" xmlns:annotation="http://schemas.microsoft.com/ado/2009/02/edm/annotation" xmlns="http://schemas.microsoft.com/ado/2008/09/edm">
23: <EntityContainer Name="EFExtensionsEntities" annotation:LazyLoadingEnabled="true">
24: <EntitySet Name="Users" EntityType="Artech.UserModel.User" />
25: </EntityContainer>
26: <EntityType Name="User">
27: <Key>
28: <PropertyRef Name="ID" />
29: </Key>
30: <Property Name="ID" Type="String" Nullable="false" MaxLength="50" Unicode="false" FixedLength="false" />
31: <Property Name="Name" Type="String" Nullable="false" MaxLength="50" Unicode="true" FixedLength="false" />
32: </EntityType>
33: </Schema>
34: </edmx:ConceptualModels>
35: <!-- C-S mapping content -->
36: <edmx:Mappings>
37: <Mapping Space="C-S" xmlns="http://schemas.microsoft.com/ado/2008/09/mapping/cs">
38: <EntityContainerMapping StorageEntityContainer="ArtechUserModelStoreContainer" CdmEntityContainer="EFExtensionsEntities">
39: <EntitySetMapping Name="Users">
40: <EntityTypeMapping TypeName="Artech.UserModel.User">
41: <MappingFragment StoreEntitySet="T_USER">
42: <ScalarProperty Name="ID" ColumnName="USER_ID" />
43: <ScalarProperty Name="Name" ColumnName="USER_NAME" />
44: </MappingFragment>
45: </EntityTypeMapping>
46: </EntitySetMapping>
47: </EntityContainerMapping>
48: </Mapping>
49: </edmx:Mappings>
50: </edmx:Runtime>
51: </edmx:Edmx>
这是新生成的.edmx文件中的XML。
1: <?xml version="1.0" encoding="utf-8"?>
2: <edmx:Edmx Version="2.0" xmlns:edmx="http://schemas.microsoft.com/ado/2008/10/edmx">
3: <!-- EF Runtime content -->
4: <edmx:Runtime>
5: <!-- SSDL content -->
6: <edmx:StorageModels>
7: <Schema Namespace="Artech.UserModel.Store" Alias="Self" Provider="System.Data.SqlClient" ProviderManifestToken="2008" xmlns:store="http://schemas.microsoft.com/ado/2007/12/edm/EntityStoreSchemaGenerator" xmlns="http://schemas.microsoft.com/ado/2009/02/edm/ssdl">
8: <EntityContainer Name="ArtechUserModelStoreContainer">
9: <EntitySet Name="T_USER" EntityType="Artech.UserModel.Store.T_USER" store:Type="Tables" Schema="dbo" />
10: </EntityContainer>
11: <EntityType Name="T_USER">
12: <Key>
13: <PropertyRef Name="USER_ID" />
14: </Key>
15: <Property Name="USER_ID" Type="varchar" Nullable="false" MaxLength="50" />
16: <Property Name="USER_NAME" Type="nvarchar" Nullable="false" MaxLength="50" />
17: </EntityType>
18: <Function Name="P_USER_I" Aggregate="false" BuiltIn="false" NiladicFunction="false" IsComposable="false" ParameterTypeSemantics="AllowImplicitConversion" Schema="dbo" >
19: <Parameter Name="p_user_id" Type="varchar" Mode="In" />
20: <Parameter Name="p_user_name" Type="nvarchar" Mode="In" />
21: </Function>
22: <Function Name="P_USER_U" Aggregate="false" BuiltIn="false" NiladicFunction="false" IsComposable="false" ParameterTypeSemantics="AllowImplicitConversion" Schema="dbo" >
23: <Parameter Name="o_user_id" Type="varchar" Mode="In" />
24: <Parameter Name="p_user_name" Type="nvarchar" Mode="In" />
25: </Function>
26: <Function Name="P_USER_D" Aggregate="false" BuiltIn="false" NiladicFunction="false" IsComposable="false" ParameterTypeSemantics="AllowImplicitConversion" Schema="dbo" >
27: <Parameter Name="o_user_id" Type="varchar" Mode="In" />
28: </Function>
29: </Schema>
30: </edmx:StorageModels>
31: <!-- CSDL content -->
32: <edmx:ConceptualModels>
33: <Schema Namespace="Artech.UserModel" Alias="Self" xmlns:annotation="http://schemas.microsoft.com/ado/2009/02/edm/annotation" xmlns="http://schemas.microsoft.com/ado/2008/09/edm">
34: <EntityContainer Name="EFExtensionsEntities" annotation:LazyLoadingEnabled="true">
35: <EntitySet Name="Users" EntityType="Artech.UserModel.User" />
36: </EntityContainer>
37: <EntityType Name="User">
38: <Key>
39: <PropertyRef Name="ID" />
40: </Key>
41: <Property Name="ID" Type="String" Nullable="false" MaxLength="50" Unicode="false" FixedLength="false" />
42: <Property Name="Name" Type="String" Nullable="false" MaxLength="50" Unicode="true" FixedLength="false" />
43: </EntityType>
44: </Schema>
45: </edmx:ConceptualModels>
46: <!-- C-S mapping content -->
47: <edmx:Mappings>
48: <Mapping Space="C-S" xmlns="http://schemas.microsoft.com/ado/2008/09/mapping/cs">
49: <EntityContainerMapping StorageEntityContainer="ArtechUserModelStoreContainer" CdmEntityContainer="EFExtensionsEntities">
50: <EntitySetMapping Name="Users">
51: <EntityTypeMapping TypeName="Artech.UserModel.User">
52: <MappingFragment StoreEntitySet="T_USER">
53: <ScalarProperty Name="ID" ColumnName="USER_ID" />
54: <ScalarProperty Name="Name" ColumnName="USER_NAME" />
55: </MappingFragment>
56: </EntityTypeMapping>
57: <EntityTypeMapping TypeName="Artech.UserModel.User" >
58: <ModificationFunctionMapping>
59: <InsertFunction FunctionName="Artech.UserModel.Store.P_USER_I">
60: <ScalarProperty Name="ID" ParameterName="p_user_id" />
61: <ScalarProperty Name="Name" ParameterName="p_user_name" />
62: </InsertFunction>
63: <UpdateFunction FunctionName="Artech.UserModel.Store.P_USER_U">
64: <ScalarProperty Name="ID" ParameterName="o_user_id" Version="Original" />
65: <ScalarProperty Name="Name" ParameterName="p_user_name" Version="Current" />
66: </UpdateFunction>
67: <DeleteFunction FunctionName="Artech.UserModel.Store.P_USER_D">
68: <ScalarProperty Name="ID" ParameterName="o_user_id" />
69: </DeleteFunction>
70: </ModificationFunctionMapping>
71: </EntityTypeMapping>
72: </EntitySetMapping>
73: </EntityContainerMapping>
74: </Mapping>
75: </edmx:Mappings>
76: </edmx:Runtime>
77: </edmx:Edmx>
顺便来看看.edmx中的数据表T_USER(只具有两个字段USER_ID和USER_NAME)和对应CUD存储过程的SQL。
1: CREATE TABLE [dbo].[T_USER]
2: (
3: [USER_ID] VARCHAR(50) PRIMARY KEY,
4: [USER_NAME] NVARCHAR(50) NOT NULL
5: )
6: GO
7:
8: CREATE PROCEDURE [dbo].[P_USER_I]
9: @p_user_id VARCHAR(50),
10: @p_user_name NVARCHAR(50)
11:
12: AS
13: BEGIN
14: INSERT T_USER(USER_ID, USER_NAME)
15: VALUES(@p_user_id,@p_user_name)
16: END
17: GO
18:
19: CREATE PROCEDURE [dbo].[P_USER_U]
20: @o_user_id VARCHAR(50),
21: @p_user_name NVARCHAR(50)
22:
23: AS
24: BEGIN
25: UPDATE T_USER
26: SET USER_NAME = @p_user_name
27: WHERE USER_ID = @o_user_id
28: END
29: GO
30: CREATE PROCEDURE [dbo].[P_USER_D]
31: @o_user_id VARCHAR(50)
32:
33: AS
34: BEGIN
35: DELETE T_USER
36: WHERE USER_ID = @o_user_id
37: END
五、局限性
EF最大的好处就是实现了概念模型和存储模型的分离。你可以为概念实体和存储实体起不同的名称,还可以将一个概念实体映射到多个存储实体,反之亦然。还可以建立概念实体的之间的继承关系。而我们这里提供的存储过程的自动映射机制,却依赖于我们预定义的标准存储过程。换句话说,我们的存储过程是完全依赖与存储模型的,而最终我们需要建立概念模型与存储过程之间的映射,当然会出现问题。
所以这种依赖于标准存储过程的映射机制基本上只能适用于概念模型与存储模型结构一致的情况。但是我相信在真正的开发中,很多人还是采用基于数据库生成.edmx模型的开发发生。如果你不对概念模型的结构(比如拆分、继承)作调整,你可以直接采用本文提供的自动映射机制。如果你需要对概念模型的结构作局部调整,由于我们生成的还是.edmx文件,你可以直接在这上面作调整。
总之一句话,如果你的概念模型和存储模型的结构相差不大,这样的自动存储过程映射机制才有意义。
在Entity Framework中使用存储过程(一):实现存储过程的自动映射
在Entity Framework中使用存储过程(二):具有继承关系实体的存储过程如何定义?
在Entity Framework中使用存储过程(三):逻辑删除的实现与自增长列值返回
在Entity Framework中使用存储过程(四):如何为Delete存储过程参数赋上Current值?
在Entity Framework中使用存储过程(五):如何通过存储过程维护多对多关系?