在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的代碼生成,了解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 #>
9: <#
10: new ProcedureMappingTemplate(this.Host.ResolvePath("UserModel.edmx"),".","EFExtensions").Render();
11: #>
通過上麵創建的TT模板(你指定的數據庫中一定要存在具有相應映射關係的存儲過程),新的.edmx模型文件會作為該tt文件的依賴文件被生成出來。而這個新生成的.edmx具有存儲過程映射信息。具體來說,下麵是原始的.edmx文件(隻保留元數據節點)。
1: <?xml version="1.0" encoding="utf-8"?>
2: <edmx:Edmx Version="2.0" xmlns:edmx="https://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="https://schemas.microsoft.com/ado/2007/12/edm/EntityStoreSchemaGenerator" xmlns="https://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="https://schemas.microsoft.com/ado/2009/02/edm/annotation" xmlns="https://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="https://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="https://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="https://schemas.microsoft.com/ado/2007/12/edm/EntityStoreSchemaGenerator" xmlns="https://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="https://schemas.microsoft.com/ado/2009/02/edm/annotation" xmlns="https://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="https://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中使用存儲過程(五):如何通過存儲過程維護多對多關係?
微信公眾賬號:大內老A
微博:www.weibo.com/artech
如果你想及時得到個人撰寫文章以及著作的消息推送,或者想看看個人推薦的技術資料,可以掃描左邊二維碼(或者長按識別二維碼)關注個人公眾號(原來公眾帳號蔣金楠的自媒體將會停用)。
本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁麵明顯位置給出原文連接,否則保留追究法律責任的權利。
最後更新:2017-10-27 13:33:40