实体数据模型 (EDM)的处理(4)

在前面几篇关于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=&quot;data source=.;initial catalog=WinFramework;integrated security=True;MultipleActiveResultSets=True;App=EntityFramework&quot;"
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=&quot;DATA SOURCE=ORCL;DBA PRIVILEGE=SYSDBA;PASSWORD=whc;PERSIST SECURITY INFO=True;USER ID=WHC&quot;"
        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=&quot;data source=.;initial catalog=WinFramework;integrated security=True;MultipleActiveResultSets=True;App=EntityFramework&quot;"
        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=&quot;server=localhost;user id=root;password=root;persistsecurityinfo=True;database=test&quot;"
        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框架的做法一致,这样可以减少每个项目都维护公用的部分内容,提高代码的重用性。

基于这个原则,我们重新设计了项目的分层关系,如下所示。

这样我们既可以减少主体项目的类数量,也可以重用公用模块的基类内容,达到更好的维护、使用的统一化处理。