在前面幾篇關於Entity Framework 實體框架的介紹里面,已經逐步對整個框架進行了一步步的演化,以期達到統一、高效、可重用性等目的,本文繼續探討基於泛型的倉儲模式實體框架方面的改進優化,使我們大家能夠很好理解其中的奧秘,並能夠達到通用的項目應用目的。本篇主要介紹實體數據模型 (EDM)的處理方面的內容。
1、實體數據模型 (EDM)的回顧
前面第一篇隨筆,我在介紹EDMX文件的時候,已經介紹過實體數據模型 (EDM),由三個概念組成:概念模型由概念架構定義語言文件 (.csdl)來定義;映射由映射規范語言文件 (.msl);存儲模型(又稱邏輯模型)由存儲架構定義語言文件 (.ssdl)來定義。
這三者合在一起就是EDM模式。EDM模式在項目中的表現形式就是擴展名為.edmx的文件。這個文件本質是一個xml文件,可以手工編輯此文件來自定義CSDL、MSL與SSDL這三部分。
CSDL定義了EDM或者說是整個程序的靈魂部分 – 概念模型。這個文件完全以程序語言的角度來定義模型的概念。即其中定義的實體、主鍵、屬性、關聯等都是對應於.NET Framework中的類型。
SSDL這個文件中描述了表、列、關系、主鍵及索引等數據庫中存在的概念。
MSL這個文件即上面所述的CSDL與SSDL的對應,主要包括CSDL中屬性與SSDL中列的對應。
2、EDMX文件的處理
我們在編譯程序的時候,發現EDMX文件並沒有生成在Debug目錄里面,而EF框架本身是需要這些對象的映射關系的,那肯定就是這些XML文件已經通過嵌入文件的方式加入到程序集里面了,我們從數據庫連接的字符串里面也可以看到端倪。
<add name="sqlserver" connectionString="metadata=res://*/Model.sqlserver.csdl|res://*/Model.sqlserver.ssdl|res://*/Model.sqlserver.msl;provider=System.Data.SqlClient;provider connection string="data source=.;initial catalog=WinFramework;integrated security=True;MultipleActiveResultSets=True;App=EntityFramework"" providerName="System.Data.EntityClient" />
我們看到,這里面提到了csdl、ssdl、msl的文件,而且這些是在資源文件的路徑,我們通過反編譯程序集可以看到,其實是確實存在這三個文件的。
但是我們並沒有把edmx文件進行拆分啊,而且也沒有把它進行文件的嵌入處理的啊?有點奇怪!
我們知道,一般這種操作可能是有針對性的自定義工具進行處理的,我們看看這個文件的屬性進行了解下。
這個edmx文件的屬性,已經包含了【自定義工具】,這個工具應該是生成對應的數據訪問上下文類代碼和實體類代碼的了,那么生成操作不是編譯或者內容,而是EntityDeploy是什么處理呢,我們通過搜索了解下。
EntityDeploy操作:一個用於部署 Entity Framework 項目的生成任務,這些項目是依據 .edmx 文件生成的。 可將這些項目作為資源嵌入,或將這些項目寫入文件。
根據這句話,我們就不難解釋,為什么編譯后的程序集自動嵌入了三個csdl、ssdl、msl的xml文件了。
如果我們想自己構建相關的數據訪問上下文類,以及實體類的代碼生成(呵呵,我想用自己的代碼生成工具統一生成,可以方便調整注釋、命名、位置等內容),雖然可以調整T4、T5模板來做這些操作,不過我覺得那個模板語言還是太啰嗦和復雜了。
這樣我把這個自定義工具【EntityModelCodeGenerator】置為空,也就是我想用自己的類定義格式,自己的生成方式去處理。當置為空的時候,我們可以看到它自動生成的類代碼刪除了,呵呵,這樣就挺好。
3、EF框架的多數據庫支持
在前面的例子里面,我們都是以默認SqlServer數據庫為例進行介紹EDMX文件,這個文件是映射的XML文件,因此對於不同的數據庫,他們之間的映射內容是有所不同的,我們可以看看SqlServer的edmx文件內容(以TB_City表為例)。
<?xml version="1.0" encoding="utf-8"?> <edmx:Edmx Version="3.0" xmlns:edmx="http://schemas.microsoft.com/ado/2009/11/edmx"> <!-- EF Runtime content --> <edmx:Runtime> <!-- SSDL content --> <edmx:StorageModels> <Schema Namespace="WinFrameworkModel.Store" Provider="System.Data.SqlClient" ProviderManifestToken="2005" Alias="Self" xmlns:store="http://schemas.microsoft.com/ado/2007/12/edm/EntityStoreSchemaGenerator" xmlns:customannotation="http://schemas.microsoft.com/ado/2013/11/edm/customannotation" xmlns="http://schemas.microsoft.com/ado/2009/11/edm/ssdl"> <EntityType Name="TB_City"> <Key> <PropertyRef Name="ID" /> </Key> <Property Name="ID" Type="bigint" StoreGeneratedPattern="Identity" Nullable="false" /> <Property Name="CityName" Type="nvarchar" MaxLength="50" /> <Property Name="ZipCode" Type="nvarchar" MaxLength="50" /> <Property Name="ProvinceID" Type="bigint" /> </EntityType> <EntityContainer Name="WinFrameworkModelStoreContainer"> <EntitySet Name="TB_City" EntityType="Self.TB_City" Schema="dbo" store:Type="Tables" /> </EntityContainer> </Schema></edmx:StorageModels>
<!-- CSDL content --> <edmx:ConceptualModels> <Schema Namespace="EntityModel" Alias="Self" annotation:UseStrongSpatialTypes="false" xmlns:annotation="http://schemas.microsoft.com/ado/2009/02/edm/annotation" xmlns:customannotation="http://schemas.microsoft.com/ado/2013/11/edm/customannotation" xmlns="http://schemas.microsoft.com/ado/2009/11/edm"> <EntityType Name="City"> <Key> <PropertyRef Name="ID" /> </Key> <Property Name="ID" Type="Int32" Nullable="false" annotation:StoreGeneratedPattern="Identity" /> <Property Name="CityName" Type="String" MaxLength="50" FixedLength="false" Unicode="true" /> <Property Name="ZipCode" Type="String" MaxLength="50" FixedLength="false" Unicode="true" /> <Property Name="ProvinceID" Type="Int32" /> </EntityType> <EntityContainer Name="SqlEntity" annotation:LazyLoadingEnabled="true"> <EntitySet Name="City" EntityType="EntityModel.City" /> </EntityContainer> </Schema> </edmx:ConceptualModels>
<!-- C-S mapping content --> <edmx:Mappings> <Mapping Space="C-S" xmlns="http://schemas.microsoft.com/ado/2009/11/mapping/cs"> <EntityContainerMapping StorageEntityContainer="WinFrameworkModelStoreContainer" CdmEntityContainer="SqlEntity"> <EntitySetMapping Name="City"> <EntityTypeMapping TypeName="EntityModel.City"> <MappingFragment StoreEntitySet="TB_City"> <ScalarProperty Name="ID" ColumnName="ID" /> <ScalarProperty Name="CityName" ColumnName="CityName" /> <ScalarProperty Name="ZipCode" ColumnName="ZipCode" /> <ScalarProperty Name="ProvinceID" ColumnName="ProvinceID" /> </MappingFragment> </EntityTypeMapping> </EntitySetMapping> </EntityContainerMapping> </Mapping> </edmx:Mappings> </edmx:Runtime> .........其他內容 </Designer> </edmx:Edmx>
而對MySql而言,它的映射關系也和這個類似,主要是SSDL部分的不同,因為具體是和數據庫相關的內容。下面是Mysql的SSDL部分的內容,從下面XML內容可以看到,里面的數據庫字段類型有所不同。
<edmx:Edmx Version="3.0" xmlns:edmx="http://schemas.microsoft.com/ado/2009/11/edmx"> <!-- EF Runtime content --> <edmx:Runtime> <!-- SSDL content --> <edmx:StorageModels> <Schema Namespace="testModel.Store" Provider="MySql.Data.MySqlClient" ProviderManifestToken="5.5" Alias="Self" xmlns:store="http://schemas.microsoft.com/ado/2007/12/edm/EntityStoreSchemaGenerator" xmlns:customannotation="http://schemas.microsoft.com/ado/2013/11/edm/customannotation" xmlns="http://schemas.microsoft.com/ado/2009/11/edm/ssdl"> <EntityType Name="tb_city"> <Key> <PropertyRef Name="ID" /> </Key> <Property Name="ID" Type="int" Nullable="false" /> <Property Name="CityName" Type="varchar" MaxLength="50" /> <Property Name="ZipCode" Type="varchar" MaxLength="50" /> <Property Name="ProvinceID" Type="int" /> </EntityType> <EntityContainer Name="testModelStoreContainer"> <EntitySet Name="tb_city" EntityType="Self.tb_city" Schema="test" store:Type="Tables" /> </EntityContainer> </Schema> </edmx:StorageModels>
從以上的對比,我們可以考慮,以一個文件為藍本,然后在代碼生成工具里面,根據不同的數據類型,映射成不同的XML文件,從而生成不同的EDMX文件即可,實體類和數據訪問上下文的類,可以是通用的,這個一點也不影響概念模型的XML內容了,所有部分變化的就是SSDL數據存儲部分的映射XML內容。
為了測試驗證,我增加了Mysql、Oracle共三個的EDMX文件,並且通過不同的配置來實現不同數據庫的訪問調用。
我們知道,數據上下文的類構建的時候,好像默認是指向具體的配置連接的,如下代碼所示(注意紅色部分)。
/// <summary> /// 數據操作上下文 /// </summary> public partial class DbEntities : DbContext { //默認的構造函數 public DbEntities() : base("name=DbEntities") { } protected override void OnModelCreating(DbModelBuilder modelBuilder) { throw new UnintentionalCodeFirstException(); } public virtual DbSet<City> City { get; set; } public virtual DbSet<Province> Province { get; set; } public virtual DbSet<DictType> DictType { get; set; } }
如果我們需要配置而不是通過代碼硬編碼方式,那么是否可以呢?否則硬編碼的方式,一次只能是指定一個特定的數據庫,也就是沒有多數據庫的配置的靈活性了。
找了很久,發現真的還是有這樣人提出這樣的問題,根據他們的解決思路,修改代碼如下所示,從而實現了配置的動態性。
/// <summary> /// 數據操作上下文 /// </summary> public partial class DbEntities : DbContext { //默認的構造函數 //public DbEntities() : base("name=DbEntities") //{ //} /// <summary> /// 動態的構造函數 /// </summary> public DbEntities() : base(nameOrConnectionString: ConnectionString()) { } /// <summary> /// 通過代碼方式,獲取連接字符串的名稱返回。 /// </summary> /// <returns></returns> private static string ConnectionString() { //根據不同的數據庫類型,構造相應的連接字符串名稱 AppConfig config = new AppConfig(); string dbType = config.AppConfigGet("ComponentDbType"); if (string.IsNullOrEmpty(dbType)) { dbType = "sqlserver"; } return string.Format("name={0}", dbType.ToLower()); } protected override void OnModelCreating(DbModelBuilder modelBuilder) { throw new UnintentionalCodeFirstException(); } public virtual DbSet<City> City { get; set; } public virtual DbSet<Province> Province { get; set; } public virtual DbSet<DictType> DictType { get; set; } }
我通過在配置文件里面,指定ComponentDbType配置項指向那個連接字符串就可以了。
<configuration> <configSections> <!-- For more information on Entity Framework configuration, visit http://go.microsoft.com/fwlink/?LinkID=237468 --> <section name="entityFramework" type="System.Data.Entity.Internal.ConfigFile.EntityFrameworkSection, EntityFramework, Version=6.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" requirePermission="false" /> <!-- For more information on Entity Framework configuration, visit http://go.microsoft.com/fwlink/?LinkID=237468 --> </configSections> <startup> <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" /> </startup> <entityFramework> <defaultConnectionFactory type="System.Data.Entity.Infrastructure.LocalDbConnectionFactory, EntityFramework"> <parameters> <parameter value="mssqllocaldb" /> </parameters> </defaultConnectionFactory> <providers> <provider invariantName="Oracle.ManagedDataAccess.Client" type="Oracle.ManagedDataAccess.EntityFramework.EFOracleProviderServices, Oracle.ManagedDataAccess.EntityFramework" /> <provider invariantName="MySql.Data.MySqlClient" type="MySql.Data.MySqlClient.MySqlProviderServices, MySql.Data.Entity.EF6"></provider> <provider invariantName="System.Data.SqlClient" type="System.Data.Entity.SqlServer.SqlProviderServices, EntityFramework.SqlServer" /> </providers> </entityFramework> <connectionStrings> <add name="oracle" connectionString="metadata=res://*/Model.oracle.csdl|res://*/Model.oracle.ssdl|res://*/Model.oracle.msl;provider=Oracle.ManagedDataAccess.Client;provider connection string="DATA SOURCE=ORCL;DBA PRIVILEGE=SYSDBA;PASSWORD=whc;PERSIST SECURITY INFO=True;USER ID=WHC"" providerName="System.Data.EntityClient" /> <add name="sqlserver" connectionString="metadata=res://*/Model.sqlserver.csdl|res://*/Model.sqlserver.ssdl|res://*/Model.sqlserver.msl;provider=System.Data.SqlClient;provider connection string="data source=.;initial catalog=WinFramework;integrated security=True;MultipleActiveResultSets=True;App=EntityFramework"" providerName="System.Data.EntityClient" /> <add name="mysql" connectionString="metadata=res://*/Model.mysql.csdl|res://*/Model.mysql.ssdl|res://*/Model.mysql.msl;provider=MySql.Data.MySqlClient;provider connection string="server=localhost;user id=root;password=root;persistsecurityinfo=True;database=test"" providerName="System.Data.EntityClient" /> </connectionStrings> <appSettings> <add key="ComponentDbType" value="mysql" /> </appSettings>
OK,這樣就很好解決了,支持多數據庫的問題了。
4、框架分層結構的提煉
我們在整個業務部分的項目里面,把一些通用的內容可以抽取到一個Common目錄層(如BaseBLL/BaseDAL等類或接口),這樣我們在BLL、DAL、IDAL、Entity目錄層,就只剩下一些和具體表相關的對象或者接口了,這樣的結構我們可能看起來會清晰一些,具體如下所示。
但是這樣雖然比原先清晰了一些,不過我們如果對基類接口進行調整的話,每個項目都可能導致不一樣了,我想把它們這些通用的基類內容抽取到一個獨立的公用模塊里面(暫定為WHC.Framework.EF項目),這樣我在所有項目里面引用他就可以了,這個做法和我在Enterprise Library框架的做法一致,這樣可以減少每個項目都維護公用的部分內容,提高代碼的重用性。
基於這個原則,我們重新設計了項目的分層關系,如下所示。
這樣我們既可以減少主體項目的類數量,也可以重用公用模塊的基類內容,達到更好的維護、使用的統一化處理。
這個系列文章索引如下:
Entity Framework 實體框架的形成之旅--基於泛型的倉儲模式的實體框架(1)
Entity Framework 實體框架的形成之旅--利用Unity對象依賴注入優化實體框架(2)
Entity Framework 實體框架的形成之旅--基類接口的統一和異步操作的實現(3)
Entity Framework 實體框架的形成之旅--實體數據模型 (EDM)的處理(4)