在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中使用存儲過程(五):如何通過存儲過程維護多對多關系?
總結
以上是生活随笔為你收集整理的在Entity Framework中使用存储过程(一):实现存储过程的自动映射的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 好的借款平台有哪些 尽量选择借款利率低的
- 下一篇: Dreamweaver——模板与库