2023年2月

在本系列的第一篇随笔《
Entity Framework 实体框架的形成之旅--基于泛型的仓储模式的实体框架(1)
》中介绍了Entity Framework 实体框架的一些基础知识,以及构建了一个简单的基于泛型的仓储模式的框架,例子也呈现了一个实体框架应用的雏形,本篇继续介绍这个主题,继续深化介绍Entity Framework 实体框架的知识,以及持续优化这个仓储模式的实体框架,主要介绍业务逻辑层的构建,以及利用Unity和反射进行动态的对象注册。

1、EDMX文件位置的调整

我们从上篇例子,可以看到这个随笔介绍的仓储模式的实体框架结构如下所示。

但实际上上篇随笔的例子是有点理想化的了,因为我们知道,【ADO.NET实体数据模型】生成的EDMX文件实质上自动生成了数据访问的上下文SqlserverContext,以及几个表的实体类,具体的效果如下所示。

我们理想化的把它放到DAL目录,Entity目录下,实际上是不可以的,至少是有冲突的。

那么我们应该如何处理,才能比较合理的处理这些自动生成的内容呢?另外我们已经把它上升了一层到业务层,具体的BLL分层如何处理数据访问对象的呢,通过什么方式构建数据访问对象?带着这些问题,我们再来一步步分析这个框架的内容。

为了给实体类友好的名称,我们顺便把表名的前缀移除了,如EDMX的图形如下所示。

为了比较好的利用EDMX文件的代码生成,我们把这个文件整体性的移动到了Entity目录下,如下所示。

这样相当于把数据访问的上下文,以及实体类的代码全部移动到Entity命名空间里面去了,虽然可能感觉不太好,但是我们先让它跑起来,具体的细节后面在优化完善。

2、业务逻辑层的设计

我们再来关注下业务逻辑层(包括业务逻辑接口层),和数据访问层类似,我们把它构建如下所示。

1)业务逻辑接口层

    /// <summary>
    ///业务逻辑层基类接口/// </summary>
    /// <typeparam name="T">实体对象类型</typeparam>
    public interface IBaseBLL<T> where T : class{                
T Get(
objectid);

IList
<T> GetAll(Expression<Func<T, bool>>whereCondition);

IList
<T>GetAll();
}

2)业务逻辑层实现

    /// <summary>
    ///业务逻辑基类/// </summary>
    /// <typeparam name="T">实体对象类型</typeparam>
    public abstract class BaseBLL<T>: IBaseBLL<T>  where T : class{protected IBaseDAL<T> baseDAL { get; set; }public BaseBLL(IBaseDAL<T>dal)
{
this.baseDAL =dal;
}
public T Get(objectid)
{
returnbaseDAL.Get(id);
}
public IList<T> GetAll(Expression<Func<T, bool>>whereCondition)
{
returnbaseDAL.GetAll(whereCondition);
}
public IList<T>GetAll()
{
returnbaseDAL.GetAll();
}
}

3)业务对象类的逻辑接口层

    /// <summary>
    ///城市的业务对象接口/// </summary>
    public interface ICityBLL : IBaseBLL<City>{
}

4)业务对象的逻辑层实现

    /// <summary>
    ///城市的业务对象/// </summary>
    public class CityBLL : BaseBLL<City>{protectedICityDAL dal;public CityBLL(ICityDAL dal) : base(dal)
{
this.dal =dal;
}
}

上面基本上完整的阐述了业务逻辑层的实现了,不过我们看到一个问题,就是不管是逻辑层基类,还是具体业务对象的逻辑对象,都没有默认构造函数,我们
不能使用new进行对象的创建!

这是一个严重的问题,那么我们如何才能规避这个问题,能够使我们的业务对象类能够使用默认函数,使用new创建对象呢?这里我们需要引入IOC容器做法,也就是使用微软的Unity进行对象的注入及使用。

3、使用Unity实现对象的依赖注入

1)Unity的简单介绍

Unity是Unity是微软patterns& practices组用C#实现的轻量级,可扩展的依赖注入容器,它为方便开发者建立松散耦合的应用程序,

有以下优点:

1.简化了对象的创建,特别是针对分层对象结构和依赖关系;

2.需求的抽象,允许开发人员在运行时或配置文件中指定依赖关系,简化横切关注点的管理;

3.推迟为容器配置组件的时机,增加了灵活性;

4.服务定位能力,这使客户能够存储或缓存容器;

5.实例和类型拦截

Unity的依赖注入使用例子比较容易理解,具体代码如下所示。

 static void Main( string[] args )
{
//实例化一个控制器 IUnityContainer unityContainer = newUnityContainer();//实现对象注入 unityContainer.RegisterType<IBird, Swallow>();
IBird bird
= unityContainer.Resolve<IBird>();

bird.Say();
}

这个Unity的对象,我们可以通过Nuget进行添加即可,添加后,在项目里面就有对应对应的程序集引用了。

2)引入Unity实现数据访问对象注入,完善逻辑层实现

了解了Unity的使用,我们可以在BaseBLL对象基类类里面构建一个IOC的容器,并在这个容器初始化的时候,注册对应的数据访问层对象即可,如下所示。

    /// <summary>
    ///业务逻辑基类/// </summary>
    /// <typeparam name="T">实体对象类型</typeparam>
    public abstract class BaseBLL<T>: IBaseBLL<T>  where T : class{private static readonly object syncRoot = newObject();protected IBaseDAL<T> baseDAL { get; set; }protected IUnityContainer container { get; set; }/// <summary>
        ///默认构造函数。///默认获取缓存的容器,如果没有则创建容器,并注册所需的接口实现。/// </summary>
        publicBaseBLL() 
{
lock(syncRoot)
{
container
= DALFactory.Instance.Container;if (container == null)
{
throw new ArgumentNullException("container", "container没有初始化");
}
}
}

好,默认在DALFactory的类里面,我们就是在其实例化的时候,把需要的数据访问对象压进去,这样我们就可以在具体的业务对象逻辑类里面实现调用,如下代码所示。

    /// <summary>
    ///城市的业务对象/// </summary>
    public class CityBLL : BaseBLL<City>{protectedICityDAL dal;publicCityBLL()
{
dal
= container.Resolve<ICityDAL>();
baseDAL
=dal;
}
public CityBLL(ICityDAL dal) : base(dal)
{
this.dal =dal;
}
}

如果我们不关心DALFactory里面的构架细节,这个框架已经完成的对象的注入,可以正常使用了。

但是我们还是来看看它的实现细节,我们通过单例模式(饿汉模式)构架IOC容器并注入相应的DAL对象了。

    /// <summary>
    ///实体框架的数据访问层接口的构造工厂。/// </summary>
    public classDALFactory
{
//普通局部变量 private static Hashtable objCache = newHashtable();private static object syncRoot = newObject();private static DALFactory m_Instance = null;/// <summary> ///IOC的容器,可调用来获取对应接口实例。/// </summary> public IUnityContainer Container { get; set; }/// <summary> ///创建或者从缓存中获取对应业务类的实例/// </summary> public staticDALFactory Instance
{
get{if (m_Instance == null)
{
lock(syncRoot)
{
if (m_Instance == null)
{
m_Instance
= newDALFactory();//初始化相关的注册接口 m_Instance.Container = new UnityContainer();//手工加载 m_Instance.Container.RegisterType<ICityDAL, CityDAL>();
m_Instance.Container.RegisterType
<IProvinceDAL, ProvinceDAL>();
}
}
}
returnm_Instance;
}
}

OK,通过上面的Unity,我们实现了对象的注入及使用个,具体的窗体调用代码如下所示。

        private void btnCity_Click(objectsender, EventArgs e)
{
DateTime dt
=DateTime.Now;

CityBLL bll
= newCityBLL();var list =bll.GetAll();this.dataGridView1.DataSource =list;

Console.WriteLine(
"花费时间:{0}", DateTime.Now.Subtract(dt).TotalMilliseconds);
}
private void txtCityName_KeyUp(objectsender, KeyEventArgs e)
{
DateTime dt
=DateTime.Now;
CityBLL bll
= newCityBLL();if(this.txtCityName.Text.Trim().Length > 0)
{
var list = bll.GetAll(s => s.CityName.Contains(this.txtCityName.Text));this.dataGridView1.DataSource =list;
}
else{var list =bll.GetAll();this.dataGridView1.DataSource =list;
}
Console.WriteLine(
"花费时间:{0}", DateTime.Now.Subtract(dt).TotalMilliseconds);
}

我们可以得到具体的界面效果如下所示。

4、使用反射操作,在Unity容器动态注册接口对象

在上面的例子里面,不知道您是否注意到了,我们使用Unity的IOC容器的时候,注册的对象是指定的几个数据访问类。

m_Instance.Container.RegisterType<ICityDAL, CityDAL>();
m_Instance.Container.RegisterType<IProvinceDAL, ProvinceDAL>();

但这种有点类似硬编码的方式,在我们项目如果有大量的这些数据访问类,需要手工添加的话,那真不是一件雅观的事情。

如果代码能够根据接口和接口实现类,自动把我们所需要的接口对象注册进去,那该是多好的啊,可是能做到吗?能!

如果我们是在同一个程序集里面执行的话,那么我们通过反射操作,就可以从这个程序集里面获取对应的接口层(IDAL)和接口实现层(DAL)的对象,那么我们匹配它进行对象注入就可以了吧。

下面是我动态注册DAL对象的实现代码,如下所示。

        /// <summary>
        ///使用Unity自动加载对应的IDAL接口的实现(DAL层)/// </summary>
        /// <param name="container"></param>
        private static voidRegisterDAL(IUnityContainer container)
{
Dictionary
<string, Type> dictInterface = new Dictionary<string, Type>();
Dictionary
<string, Type> dictDAL = new Dictionary<string, Type>();
Assembly currentAssembly
=Assembly.GetExecutingAssembly();string dalSuffix = ".DAL";string interfaceSuffix = ".IDAL";//对比程序集里面的接口和具体的接口实现类,把它们分别放到不同的字典集合里 foreach (Type objType incurrentAssembly.GetTypes())
{
string defaultNamespace =objType.Namespace;if (objType.IsInterface &&defaultNamespace.EndsWith(interfaceSuffix))
{
if (!dictInterface.ContainsKey(objType.FullName))
{
dictInterface.Add(objType.FullName, objType);
}
}
else if(defaultNamespace.EndsWith(dalSuffix))
{
if (!dictDAL.ContainsKey(objType.FullName))
{
dictDAL.Add(objType.FullName, objType);
}
}
}
//根据注册的接口和接口实现集合,使用IOC容器进行注册 foreach (string key indictInterface.Keys)
{
Type interfaceType
=dictInterface[key];foreach (string dalKey indictDAL.Keys)
{
Type dalType
=dictDAL[dalKey];if (interfaceType.IsAssignableFrom(dalType))//判断DAL是否实现了某接口 {
container.RegisterType(interfaceType, dalType);
}
}
}
}

有了这个利用反射动态注入对象的操作,我们在基类里面的实现就避免了硬编码的不便。

    /// <summary>
    ///实体框架的数据访问层接口的构造工厂。/// </summary>
    public classDALFactory
{
//普通局部变量 private static Hashtable objCache = newHashtable();private static object syncRoot = newObject();private static DALFactory m_Instance = null;/// <summary> ///IOC的容器,可调用来获取对应接口实例。/// </summary> public IUnityContainer Container { get; set; }/// <summary> ///创建或者从缓存中获取对应业务类的实例/// </summary> public staticDALFactory Instance
{
get{if (m_Instance == null)
{
lock(syncRoot)
{
if (m_Instance == null)
{
m_Instance
= newDALFactory();//初始化相关的注册接口 m_Instance.Container = newUnityContainer();//根据约定规则自动注册DAL RegisterDAL(m_Instance.Container);//手工加载//m_Instance.Container.RegisterType<ICityDAL, CityDAL>();//m_Instance.Container.RegisterType<IProvinceDAL, ProvinceDAL>(); }
}
}
returnm_Instance;
}
}

上面整个框架的优化过程,都是围绕着业务逻辑层进行的,最后我们实现了较好的动态对象的依赖注入,并给业务逻辑层对象提供了默认构造函数,让他们可以从IOC容器里面获取对象并创建。

但是我们看到,对于EDMX文件,我们只是把它放入了Entity的模块里面,也没有真正的对它如何处理,如果每次都需要使用这个edmx的文件生成操作,我依旧觉得开发效率比较低下,而且如果对于需要支持多个数据库如何处理呢?不可能在创建一个数据操作上下文吧,它们可以已经抽象化了,本身好像不是和具体数据库相关的,和数据库相关的只是它的配置关系而已啊。

这些问题留给下一篇继续对框架的演化处理吧,谢谢大家耐心的阅读,如果觉得有用,请继续推荐支持下,毕竟为了准备这个系列,我已经花了好多天的时间,从各个方面持续优化整个仓储模式的实体框架,留下一个个版本的Demo来整理博客的。

这个系列文章索引如下:

Entity Framework 实体框架的形成之旅--基于泛型的仓储模式的实体框架(1)

Entity Framework 实体框架的形成之旅--利用Unity对象依赖注入优化实体框架(2)

Entity Framework 实体框架的形成之旅--基类接口的统一和异步操作的实现(3)

在本系列的第一篇随笔《
Entity Framework 实体框架的形成之旅--基于泛型的仓储模式的实体框架(1)
》中介绍了Entity Framework 实体框架的一些基础知识,以及构建了一个简单的基于泛型的仓储模式的框架;在随笔《
Entity Framework 实体框架的形成之旅--利用Unity对象依赖注入优化实体框架(2)
》则持续优化这个仓储模式的实体框架,主要介绍业务逻辑层的构建,以及利用Unity和反射进行动态的对象注册。本篇主要介绍基类接口的统一和异步操作的实现等方面,逐步把我框架接口命名的方式进行统一,并增加所有必要用到的增删改查、分页、lambda表达式条件处理,以及异步操作等特性,这样能够尽可能的符合基类这个特殊类的定义,实现功能接口的最大化重用和统一。

1、基类接口的统一命名和扩展

在我以前的基于Enterprise Library的框架里面,定义了一个超级的数据访问基类,是特定数据访问类基类的基类,AbstractBaseDAL的数据访问层基类定义了很多通用的接口,具有非常强大的操作功能,如下所示。

这里面的很多接口命名我都经过了一些推敲,或者我基于我或者我客户群体的使用习惯和理解考虑,也是想沿承这些命名规则,扩充我这个基于泛型的仓储模式的实体框架基类接口。

下面是各类不同接口的定义内容。

1)增加操作

        /// <summary>
        ///插入指定对象到数据库中/// </summary>
        /// <param name="t">指定的对象</param>
        /// <returns>执行成功返回<c>true</c>,否则为<c>false</c></returns>
        bool Insert(T t);

2)删除操作

        /// <summary>
        ///根据指定对象的ID,从数据库中删除指定对象/// </summary>
        /// <param name="id">对象的ID</param>
        /// <returns>执行成功返回<c>true</c>,否则为<c>false</c></returns>
        bool Delete(objectid);/// <summary>
        ///从数据库中删除指定对象/// </summary>
        /// <param name="id">指定对象</param>
        /// <returns>执行成功返回<c>true</c>,否则为<c>false</c></returns>
        bool Delete(T t);

3)修改操作

        /// <summary>
        ///更新对象属性到数据库中/// </summary>
        /// <param name="t">指定的对象</param>
        /// <param name="key">主键的值</param>
        /// <returns>执行成功返回<c>true</c>,否则为<c>false</c></returns>
        bool Update(T t, object key);

4)主键查询以及条件查询操作

        /// <summary>
        ///查询数据库,返回指定ID的对象/// </summary>
        /// <param name="id">ID主键的值</param>
        /// <returns>存在则返回指定的对象,否则返回Null</returns>
        T FindByID(objectid);/// <summary>
        ///根据条件查询数据库,如果存在返回第一个对象/// </summary>
        /// <param name="match">条件表达式</param>
        /// <returns>存在则返回指定的第一个对象,否则返回默认值</returns>
        T FindSingle(Expression<Func<T, bool>> match);

5)集合查询(分返回IQueryable和ICollection<T>两种方式)

        /// <summary>
        ///返回可查询的记录源/// </summary>
        /// <returns></returns>
        IQueryable<T>GetQueryable();/// <summary>
        ///根据条件表达式返回可查询的记录源/// </summary>
        /// <param name="match">查询条件</param>
        /// <param name="orderByProperty">排序表达式</param>
        /// <param name="isDescending">如果为true则为降序,否则为升序</param>
        /// <returns></returns>
        IQueryable<T> GetQueryable(Expression<Func<T, bool>> match, string sortPropertyName, bool isDescending = true);/// <summary>
        ///根据条件查询数据库,并返回对象集合/// </summary>
        /// <param name="match">条件表达式</param>
        /// <returns></returns>
        ICollection<T> Find(Expression<Func<T, bool>>match);/// <summary>
        ///根据条件查询数据库,并返回对象集合/// </summary>
        /// <param name="match">条件表达式</param>
        /// <param name="orderByProperty">排序表达式</param>
        /// <param name="isDescending">如果为true则为降序,否则为升序</param>
        /// <returns></returns>
        ICollection<T> Find<TKey>(Expression<Func<T, bool>> match, Expression<Func<T, TKey>> orderByProperty, bool isDescending = true);

6)分页查询操作

        /// <summary>
        ///根据条件查询数据库,并返回对象集合(用于分页数据显示)/// </summary>
        /// <param name="match">条件表达式</param>
        /// <param name="info">分页实体</param>
        /// <param name="orderByProperty">排序表达式</param>
        /// <param name="isDescending">如果为true则为降序,否则为升序</param>
        /// <returns>指定对象的集合</returns>
        ICollection<T> FindWithPager(Expression<Func<T, bool>>match, PagerInfo info);/// <summary>
        ///根据条件查询数据库,并返回对象集合(用于分页数据显示)/// </summary>
        /// <param name="match">条件表达式</param>
        /// <param name="info">分页实体</param>
        /// <param name="orderByProperty">排序表达式</param>
        /// <param name="isDescending">如果为true则为降序,否则为升序</param>
        /// <returns>指定对象的集合</returns>
        ICollection<T> FindWithPager<TKey>(Expression<Func<T, bool>> match, PagerInfo info, Expression<Func<T, TKey>> orderByProperty, bool isDescending = true);

这样我们在BaseDAL里面,把这些接口全部实现了,那么所有继承这个基类对象的数据访问对象,就具有这些标准的接口了,也给我们开发实现了整体性的统一。

首先我们来看看这个基类BaseDAL的初始化定义代码。

    /// <summary>
    ///数据访问层基类/// </summary>
    /// <typeparam name="T">实体对象类型</typeparam>
    public abstract class BaseDAL<T> : IBaseDAL<T>  where T : class{#region 变量及构造函数

        /// <summary>
        ///DbContext对象/// </summary>
        protectedDbContext baseContext;/// <summary>
        ///指定类型的实体对象集合/// </summary>
        protected DbSet<T>objectSet;/// <summary>
        ///是否为降序/// </summary>
        public bool IsDescending { get; set; }/// <summary>
        ///排序属性/// </summary>
        public string SortPropertyName { get; set; }/// <summary>
        ///参数化构造函数/// </summary>
        /// <param name="context">DbContext对象</param>
        publicBaseDAL(DbContext context)
{
this.baseContext = context;
this.objectSet = this.baseContext.Set<T>();
this.IsDescending = true;this.SortPropertyName = "ID";
}
#endregion

有了这些DbContext对象以及DbSet<T>对象,具体的接口实现就很容易了,下面我抽几个代表性的函数来介绍实现。

1)增加对象

        /// <summary>
        ///插入指定对象到数据库中/// </summary>
        /// <param name="t">指定的对象</param>
        /// <returns>执行成功返回<c>true</c>,否则为<c>false</c></returns>
        public virtual boolInsert(T t)
{
ArgumentValidation.CheckForNullReference(t,
"传入的对象t为空");

objectSet.Add(t);
return baseContext.SaveChanges() > 0;
}

2)删除对象

        /// <summary>
        ///根据指定对象的ID,从数据库中删除指定对象/// </summary>
        /// <param name="id">对象的ID</param>
        /// <returns>执行成功返回<c>true</c>,否则为<c>false</c></returns>
        public virtual bool Delete(objectid)
{
T obj
=objectSet.Find(id);
objectSet.Remove(obj);
return baseContext.SaveChanges() > 0;
}

3)修改对象

        /// <summary>
        ///更新对象属性到数据库中/// </summary>
        /// <param name="t">指定的对象</param>
        /// <param name="key">主键的值</param>
        /// <returns>执行成功返回<c>true</c>,否则为<c>false</c></returns>
        public virtual bool Update(T t, objectkey)
{
ArgumentValidation.CheckForNullReference(t,
"传入的对象t为空");bool result = false;
T existing
=objectSet.Find(key);if (existing != null)
{
baseContext.Entry(existing).CurrentValues.SetValues(t);
result
= baseContext.SaveChanges() > 0;
}
returnresult;
}

4)根据条件查询

        /// <summary>
        ///根据条件查询数据库,如果存在返回第一个对象/// </summary>
        /// <param name="match">条件表达式</param>
        /// <returns>存在则返回指定的第一个对象,否则返回默认值</returns>
        public virtual T FindSingle(Expression<Func<T, bool>>match)
{
returnobjectSet.FirstOrDefault(match);
}
/// <summary> ///根据条件表达式返回可查询的记录源/// </summary> /// <param name="match">查询条件</param> /// <param name="orderByProperty">排序表达式</param> /// <param name="isDescending">如果为true则为降序,否则为升序</param> /// <returns></returns> public virtual IQueryable<T> GetQueryable(Expression<Func<T, bool>> match, string sortPropertyName, bool isDescending = true)
{
IQueryable
<T> query = this.objectSet;if (match != null)
{
query
=query.Where(match);
}
returnquery.OrderBy(sortPropertyName, isDescending);
}

5)分页查询

        /// <summary>
        ///根据条件查询数据库,并返回对象集合(用于分页数据显示)/// </summary>
        /// <param name="match">条件表达式</param>
        /// <param name="info">分页实体</param>
        /// <param name="orderByProperty">排序表达式</param>
        /// <param name="isDescending">如果为true则为降序,否则为升序</param>
        /// <returns>指定对象的集合</returns>
        public virtual ICollection<T> FindWithPager(Expression<Func<T, bool>>match, PagerInfo info)
{
int pageindex = (info.CurrenetPageIndex < 1) ? 1: info.CurrenetPageIndex;int pageSize = (info.PageSize <= 0) ? 20: info.PageSize;int excludedRows = (pageindex - 1) *pageSize;

IQueryable
<T> query =GetQueryable().Where(match);
info.RecordCount
=query.Count();returnquery.Skip(excludedRows).Take(pageSize).ToList();
}

更多的代码就不一一贴出,反正我们全部实现自己所需的各种操作就可以了,这里要提的是,我们尽可能利用Lambda表达式进行条件处理,包括查询、删除等条件处理。

对上面的这些常规接口,我们调用代码处理的例子如下所示。

       private void btnProvince_Click(objectsender, EventArgs e)
{
DateTime dt
=DateTime.Now;var list = BLLFactory<ProvinceBLL>.Instance.GetAll(s=>s.ProvinceName);this.dataGridView1.DataSource =list;

Console.WriteLine(
"花费时间:{0}", DateTime.Now.Subtract(dt).TotalMilliseconds);
}
private void btnCity_Click(objectsender, EventArgs e)
{
DateTime dt
=DateTime.Now;

CityBLL bll
= newCityBLL();var result =bll.GetAll();this.dataGridView1.DataSource =result;

Console.WriteLine(
"花费时间:{0}", DateTime.Now.Subtract(dt).TotalMilliseconds);
}

如果需要考虑分页,以上接口已经定义了分页处理的接口和实现了,我们在业务对象里面直接调用接口就可以了,具体代码如下所示。

                CityBLL bll = newCityBLL();
PagerInfo info
= newPagerInfo();
info.PageSize
= 30;
info.CurrenetPageIndex
=1;

ICollection
<City>list;if (i++ % 2 == 0)
{
sortType
= "自定义排序";//使用自定义排序 list = bll.FindWithPager(s => s.CityName.Contains("南"), info, o => o.ID, true);
}
else{
sortType
= "默认字段排序";//使用默认字段排序 list = bll.FindWithPager(s => s.CityName.Contains("南"), info);
}
this.dataGridView1.DataSource = list;

2、异步操作的定义和调用

在EF里面实现异步(并行)非常容易,在.NET 4.5里由于async/await关键字的出现,使得实现异步变得更加容易。

使用await关键字后,.NET会自动把返回结果包装在一个Task类型的对象中。使用await表达式时,控制会返回到调用此方法的线程中;在await等待的方法执行完毕后,控制会自动返回到下面的语句中。发生异常时,异常会在await表达式中抛出。

我们基本上所有的增删改查、分页等接口,都可以使用异步操作来定义这些新接口,代码如下所示。

1)增加对象异步实现

异步定义的接口如下所示

        /// <summary>
        ///插入指定对象到数据库中(异步)/// </summary>
        /// <param name="t">指定的对象</param>
        /// <returns>执行成功返回<c>true</c>,否则为<c>false</c></returns>
        Task<bool> InsertAsync(T t);

接口的实现如下所示

        /// <summary>
        ///插入指定对象到数据库中(异步)/// </summary>
        /// <param name="t">指定的对象</param>
        /// <returns>执行成功返回<c>true</c>,否则为<c>false</c></returns>
        public virtual async Task<bool>InsertAsync(T t)
{
ArgumentValidation.CheckForNullReference(t,
"传入的对象t为空");

objectSet.Add(t);
return await baseContext.SaveChangesAsync() > 0;
}

和普通的接口定义不一样的地方,我们看到异步的接口都是以Async结尾,并且返回值使用Task<T>进行包装,另外实现里面,增加了async的定义,方法体里面增加 await 的关键字,这些就构成了异步操作的接口定义和接口实现了。

2)条件删除异步实现

我们再来看一个复杂一点的条件删除操作,代码如下所示。

定义接口

        /// <summary>
        ///根据指定条件,从数据库中删除指定对象(异步)/// </summary>
        /// <param name="match">条件表达式</param>
        /// <returns>执行成功返回<c>true</c>,否则为<c>false</c></returns>
        Task<bool> DeleteByConditionAsync(Expression<Func<T, bool>> match);

接口实现

        /// <summary>
        ///根据指定条件,从数据库中删除指定对象(异步)/// </summary>
        /// <param name="match">条件表达式</param>
        /// <returns>执行成功返回<c>true</c>,否则为<c>false</c></returns>
        public virtual async Task<bool> DeleteByConditionAsync(Expression<Func<T, bool>>match)
{
objectSet.Where
<T>(match).ToList<T>().ForEach(d => baseContext.Entry<T>(d).State =EntityState.Deleted);return await baseContext.SaveChangesAsync() > 0;
}

我们定义的这些异步接口,基本上都是类似的操作,但是我们应该如何调用异步的处理呢?

好像有两个调用代码方式。

1)使用async和await关键字处理

        private asyncvoid btnCity_Click(objectsender, EventArgs e)
{
DateTime dt
=DateTime.Now;

CityBLL bll
= newCityBLL();var result = awaitbll.GetAllAsync();this.dataGridView1.DataSource =result;

Console.WriteLine(
"花费时间:{0}", DateTime.Now.Subtract(dt).TotalMilliseconds);
}

2)使用 await Task.Run的处理方式

        private async void btnCity_Click(objectsender, EventArgs e)
{
DateTime dt
=DateTime.Now;

CityBLL bll
= newCityBLL();var result = await Task.Run(() =>{var list =bll.GetAllAsync();returnlist;
});
this.dataGridView1.DataSource =result;

Console.WriteLine(
"花费时间:{0}", DateTime.Now.Subtract(dt).TotalMilliseconds);
}

两种方式都能正常运行,并得到所要的效果。

本篇主要介绍了基类接口的统一封装、并增加所有必要的增删改查、分页查询、Lambda条件等处理方式,还有就是增加了相关的异步操作接口和实现,随着我们对通用功能的进一步要求,可以为基类增加更多的接口函数。

这个系列文章索引如下:

Entity Framework 实体框架的形成之旅--基于泛型的仓储模式的实体框架(1)

Entity Framework 实体框架的形成之旅--利用Unity对象依赖注入优化实体框架(2)

Entity Framework 实体框架的形成之旅--基类接口的统一和异步操作的实现(3)

在前面几篇关于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文件已经通过嵌入文件的方式加入到程序集里面了,我们从数据库连接的字符串里面也可以看到端倪。

    <addname="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:EdmxVersion="3.0"xmlns:edmx="http://schemas.microsoft.com/ado/2009/11/edmx">
  <!--EF Runtime content-->
  <edmx:Runtime>
    <!--SSDL content-->
    <edmx:StorageModels>
    <SchemaNamespace="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">
        <EntityTypeName="TB_City">
          <Key>
            <PropertyRefName="ID" />
          </Key>
          <PropertyName="ID"Type="bigint"StoreGeneratedPattern="Identity"Nullable="false" />
          <PropertyName="CityName"Type="nvarchar"MaxLength="50" />
          <PropertyName="ZipCode"Type="nvarchar"MaxLength="50" />
          <PropertyName="ProvinceID"Type="bigint" />
        </EntityType>
        <EntityContainerName="WinFrameworkModelStoreContainer">
          <EntitySetName="TB_City"EntityType="Self.TB_City"Schema="dbo"store:Type="Tables" />
        </EntityContainer>
      </Schema></edmx:StorageModels>

<!--CSDL content--> <edmx:ConceptualModels> <SchemaNamespace="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"> <EntityTypeName="City"> <Key> <PropertyRefName="ID" /> </Key> <PropertyName="ID"Type="Int32"Nullable="false"annotation:StoreGeneratedPattern="Identity" /> <PropertyName="CityName"Type="String"MaxLength="50"FixedLength="false"Unicode="true" /> <PropertyName="ZipCode"Type="String"MaxLength="50"FixedLength="false"Unicode="true" /> <PropertyName="ProvinceID"Type="Int32" /> </EntityType> <EntityContainerName="SqlEntity"annotation:LazyLoadingEnabled="true"> <EntitySetName="City"EntityType="EntityModel.City" /> </EntityContainer> </Schema> </edmx:ConceptualModels>

<!--C-S mapping content--> <edmx:Mappings> <MappingSpace="C-S"xmlns="http://schemas.microsoft.com/ado/2009/11/mapping/cs"> <EntityContainerMappingStorageEntityContainer="WinFrameworkModelStoreContainer"CdmEntityContainer="SqlEntity"> <EntitySetMappingName="City"> <EntityTypeMappingTypeName="EntityModel.City"> <MappingFragmentStoreEntitySet="TB_City"> <ScalarPropertyName="ID"ColumnName="ID" /> <ScalarPropertyName="CityName"ColumnName="CityName" /> <ScalarPropertyName="ZipCode"ColumnName="ZipCode" /> <ScalarPropertyName="ProvinceID"ColumnName="ProvinceID" /> </MappingFragment> </EntityTypeMapping> </EntitySetMapping> </EntityContainerMapping> </Mapping> </edmx:Mappings> </edmx:Runtime>.........其他内容</Designer> </edmx:Edmx>

而对MySql而言,它的映射关系也和这个类似,主要是SSDL部分的不同,因为具体是和数据库相关的内容。下面是Mysql的SSDL部分的内容,从下面XML内容可以看到,里面的数据库字段类型有所不同。

<edmx:EdmxVersion="3.0"xmlns:edmx="http://schemas.microsoft.com/ado/2009/11/edmx">
  <!--EF Runtime content-->
  <edmx:Runtime>
    <!--SSDL content-->
    <edmx:StorageModels>
      <SchemaNamespace="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">
        <EntityTypeName="tb_city">
          <Key>
            <PropertyRefName="ID" />
          </Key>
          <PropertyName="ID"Type="int"Nullable="false" />
          <PropertyName="CityName"Type="varchar"MaxLength="50" />
          <PropertyName="ZipCode"Type="varchar"MaxLength="50" />
          <PropertyName="ProvinceID"Type="int" />
        </EntityType>

        <EntityContainerName="testModelStoreContainer">
          <EntitySetName="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 classDbEntities : DbContext
{
//默认的构造函数 public DbEntities() : base("name=DbEntities")
{
}
protected override voidOnModelCreating(DbModelBuilder modelBuilder)
{
throw newUnintentionalCodeFirstException();
}
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 classDbEntities : DbContext
{
//默认的构造函数//public DbEntities() : base("name=DbEntities")//{//} /// <summary> ///动态的构造函数/// </summary> public DbEntities() : base(nameOrConnectionString: ConnectionString())
{
}
/// <summary> ///通过代码方式,获取连接字符串的名称返回。/// </summary> /// <returns></returns> private static stringConnectionString()
{
//根据不同的数据库类型,构造相应的连接字符串名称 AppConfig config = newAppConfig();string dbType = config.AppConfigGet("ComponentDbType");if (string.IsNullOrEmpty(dbType))
{
dbType
= "sqlserver";
}
return string.Format("name={0}", dbType.ToLower());
}
protected override voidOnModelCreating(DbModelBuilder modelBuilder)
{
throw newUnintentionalCodeFirstException();
}
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-->
    <sectionname="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>
    <supportedRuntimeversion="v4.0"sku=".NETFramework,Version=v4.5" />
  </startup>
  <entityFramework>
    <defaultConnectionFactorytype="System.Data.Entity.Infrastructure.LocalDbConnectionFactory, EntityFramework">
      <parameters>
        <parametervalue="mssqllocaldb" />
      </parameters>
    </defaultConnectionFactory>
    <providers>
      <providerinvariantName="Oracle.ManagedDataAccess.Client"type="Oracle.ManagedDataAccess.EntityFramework.EFOracleProviderServices, Oracle.ManagedDataAccess.EntityFramework" />
      <providerinvariantName="MySql.Data.MySqlClient"type="MySql.Data.MySqlClient.MySqlProviderServices, MySql.Data.Entity.EF6"></provider>
      <providerinvariantName="System.Data.SqlClient"type="System.Data.Entity.SqlServer.SqlProviderServices, EntityFramework.SqlServer" />
    </providers>
  </entityFramework>
  <connectionStrings>
    <addname="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" />
    <addname="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" />
    <addname="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>
    <addkey="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)

在前面几篇介绍了Entity Framework 实体框架的形成过程,整体框架主要是基于Database First的方式构建,也就是利用EDMX文件的映射关系,构建表与表之间的关系,这种模式弹性好,也可以利用图形化的设计器来设计表之间的关系,是开发项目较多采用的模式,不过问题还是这个XML太过复杂,因此有时候也想利用Code First模式构建整个框架。本文主要介绍利用Code First 来构建整个框架的过程以及碰到的问题探讨。

1、基于SqlServer的Code First模式

为了快速了解Code First的工作模式,我们先以微软自身的SQLServer数据库进行开发测试,我们还是按照常规的模式先构建一个标准关系的数据库,如下所示。

这个表包含了几个经典的关系,一个是自引用关系的Role表,一个是User和Role表的多对多关系,一个是User和UserDetail之间的引用关系。

一般情况下,能处理好这几种关系,基本上就能满足大多数项目上的要求了。这几个表的数据库脚本如下所示。

create tabledbo.Role (
ID
nvarchar(50) not null,
Name
nvarchar(50) null,
ParentID
nvarchar(50) null,constraint PK_ROLE primary key(ID)
)
go create table dbo."User" (
ID
nvarchar(50) not null,
Account
nvarchar(50) null,
Password
nvarchar(50) null,constraint PK_USER primary key(ID)
)
go create tabledbo.UserDetail (
ID
nvarchar(50) not null,User_ID nvarchar(50) null,
Name
nvarchar(50) null,
Sex
int null,
Birthdate
datetime null,
Height
decimal null,
Note
ntext null,constraint PK_USERDETAIL primary key(ID)
)
go create tabledbo.UserRole (User_ID nvarchar(50) not null,
Role_ID
nvarchar(50) not null,constraint PK_USERROLE primary key (User_ID, Role_ID)
)
go alter tabledbo.Roleadd constraint FK_ROLE_REFERENCE_ROLE foreign key(ParentID)referencesdbo.Role (ID)go alter tabledbo.UserDetailadd constraint FK_USERDETA_REFERENCE_USER foreign key (User_ID)references dbo."User" (ID)go alter tabledbo.UserRoleadd constraint FK_USERROLE_REFERENCE_ROLE foreign key(Role_ID)referencesdbo.Role (ID)go alter tabledbo.UserRoleadd constraint FK_USERROLE_REFERENCE_USER foreign key (User_ID)references dbo."User" (ID)go

我们采用刚才介绍的Code Frist方式来构建实体框架,如下面几个步骤所示。

1)选择来自数据库的Code First方式。

2)选择指定的数据库连接,并选择对应的数据库表,如下所示(包括中间表UserRole)。

生成项目后,项目工程会增加几个类,包括Role实体类,User实体类,UserDetail实体类(没有中间表UserRole的实体类),还有一个是包含这些实体类的数据库上下文关系,它们的表之间的关系,是通过代码指定的,没有了EDMX文件了。

几个类文件的代码如下所示,其中实体类在类定义的头部,
增加了[Table("Role")]的说明
,表明了这个实体类和数据库表之间的关系。

    [Table("Role")]
    publicpartial class Role
{
publicRole()
{
Children
= new HashSet<Role>();
Users
= new HashSet<User>();
}
[StringLength(50)] public string ID { get; set; }[StringLength(50)] public string Name { get; set; }[StringLength(50)] public string ParentID { get; set; }public virtual ICollection<Role> Children { get; set; }public virtual Role Parent { get; set; }public virtual ICollection<User> Users { get; set; }
}

其他类如下所示。

    [Table("User")]
    public partial class User{public User()
{
UserDetails
= new HashSet<UserDetail>();
Roles
= new HashSet<Role>();
}
[StringLength(50)] public string ID { get; set; }[StringLength(50)] public string Account { get; set; }[StringLength(50)] public string Password { get; set; }public virtual ICollection<UserDetail> UserDetails { get; set; }public virtual ICollection<Role> Roles { get; set; }
}
    [Table("UserDetail")]
    publicpartial class UserDetail
{
[StringLength(50)] public string ID { get; set; }[StringLength(50)] public string User_ID { get; set; }[StringLength(50)] public string Name { get; set; }public int? Sex { get; set; }public DateTime? Birthdate { get; set; }public decimal? Height { get; set; }[Column(TypeName = "ntext")] public string Note { get; set; }public virtual User User { get; set; }
}

还有一个就是生成的数据库上下文的类。

    public partial classDbEntities : DbContext
{
public DbEntities() : base("name=Model1")
{
}
public virtual DbSet<Role> Roles { get; set; }public virtual DbSet<User> Users { get; set; }public virtual DbSet<UserDetail> UserDetails { get; set; }protected override voidOnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Entity
<Role>()
.HasMany(e
=>e.Children)
.WithOptional(e
=>e.Parent)
.HasForeignKey(e
=>e.ParentID);

modelBuilder.Entity
<Role>()
.HasMany(e
=>e.Users)
.WithMany(e
=>e.Roles)
.Map(m
=> m.ToTable("UserRole"));

modelBuilder.Entity
<User>()
.HasMany(e
=>e.UserDetails)
.WithOptional(e
=>e.User)
.HasForeignKey(e
=>e.User_ID);

modelBuilder.Entity
<UserDetail>()
.Property(e
=>e.Height)
.HasPrecision(
18, 0);
}
}

上面这个数据库上下文的操作类,通过在OnModelCreating函数里面使用代码方式指定了几个表之间的关系,代替了EDMX文件的描述。

这样好像看起来比EDMX文件简单了很多,感觉很开心,一切就那么顺利。

如果我们使用这个数据库上下文进行数据库的插入,也是很顺利的执行,并包含了的多个表之间的关系处理,代码如下所示。

        private voidNormalTest()
{
DbEntities db
= newDbEntities();
Role role
= new Role() { ID = Guid.NewGuid().ToString(), Name = "test33"};

User user
= new User() { ID = Guid.NewGuid().ToString(), Account = "test33", Password = "test33"};
UserDetail detail
= new UserDetail() { ID = Guid.NewGuid().ToString(), Name = "userName33", Sex = 1, Note = "测试内容33", Height = 175};
user.UserDetails.Add(detail);

role.Users.Add(user);

db.Roles.Add(role);
db.SaveChanges();

List
<Role> list =db.Roles.ToList();
}

我们发现,通过上面代码的操作,几个表都写入了数据,已经包含了他们之间的引用关系了。

2、基于泛型的仓储模式实体框架的提炼

为了更好对不同数据库的封装,我引入了前面介绍的基于泛型的仓储模式实体框架的结构,希望后面能够兼容多种数据库的支持,最终构建代码的分层结构如下所示。

使用这种框架的分层,相当于为各个数据库访问提供了统一标准的通用接口,为我们利用各种强大的基类快速实现各种功能提供了很好的保障。使用这种分层的框架代码如下所示。

        private voidFrameworkTest()
{
Role role
= new Role() { ID = Guid.NewGuid().ToString(), Name = "test33"};

User user
= new User() { ID = Guid.NewGuid().ToString(), Account = "test33", Password = "test33"};
UserDetail detail
= new UserDetail() { ID = Guid.NewGuid().ToString(), Name = "userName33", Sex = 1, Note = "测试内容33", Height = 175};
user.UserDetails.Add(detail);

role.Users.Add(user);

IFactory.Instance
<IRoleBLL>().Insert(role);

ICollection
<Role> list = IFactory.Instance<IRoleBLL>().GetAll();

}

我们发现,这部分代码执行的效果和纯粹使用自动生成的数据库上下文DbEntities 来操作数据库一样,能够写入各个表的数据,并添加了相关的应用关系。

满以为这样也可以很容易扩展到Oracle数据库上,但使用SQLServer数据库生成的实体类,在Oracle数据库访问的时候,发现它生成的实体类名称全部是大写,一旦修改为Camel驼峰格式的字段,就会出现找不到对应表字段的错误。

寻找了很多解决方案,依旧无法有效避免这个问题,因为Oracle本身的表或者字段名称是大小写敏感的,关于Oracle这个问题,先关注后续解决吧,不过对于如果不考虑支持多种数据库的话,基于SQLServer数据库的Code First构建框架真的还是比较方便,我们不用维护那个比较麻烦的EDMX文件,只需要在代码函数里面动态添加几个表之间的关系即可。

这个系列文章索引如下:

Entity Framework 实体框架的形成之旅--基于泛型的仓储模式的实体框架(1)

Entity Framework 实体框架的形成之旅--利用Unity对象依赖注入优化实体框架(2)

Entity Framework 实体框架的形成之旅--基类接口的统一和异步操作的实现(3)

Entity Framework 实体框架的形成之旅--实体数据模型 (EDM)的处理(4)

Entity Framework 实体框架的形成之旅--Code First的框架设计(5)

在前面的随笔《
Entity Framework 实体框架的形成之旅--Code First的框架设计(5)
》里介绍了基于Code First模式的实体框架的经验,这种方式自动处理出来的模式是通过在实体类(POCO

)里面添加相应的特性说明来实现的,但是有时候我们可能需要考虑基于多种数据库的方式,那这种方式可能就不合适。本篇主要介绍使用 Fluent API 配置实现Code First模式的实体框架构造方式。

使用实体框架 Code First 时,默认行为是使用一组 EF 中内嵌的约定将 POCO 类映射到表。但是,有时您无法或不想遵守这些约定,需要将实体映射到约定指示外的其他对象。特别是这些内嵌的约定可能和数据库相关的,对不同的数据库可能有不同的表示方式,或者我们可能不同数据库的表名、字段名有所不同;还有就是我们希望尽可能保持POCO类的纯洁度,不希望弄得太过乌烟瘴气的,那么我们这时候引入Fluent API 配置就很及时和必要了。

1、Code First模式的代码回顾

上篇随笔里面我构造了几个代表性的表结构,具体关系
如下所示。

这些表包含了几个经典的关系,一个是自引用关系的Role表,一个是User和Role表的多对多关系,一个是User和UserDetail之间的引用关系。

我们看到,默认使用EF工具自动生成的实体类代码如下所示。

[Table("Role")]public partial classRole
{
publicRole()
{
Children
= new HashSet<Role>();
Users
= new HashSet<User>();
}

[StringLength(
50)]public string ID { get; set; }

[StringLength(
50)]public string Name { get; set; }

[StringLength(
50)]public string ParentID { get; set; }public virtual ICollection<Role> Children { get; set; }public virtual Role Parent { get; set; }public virtual ICollection<User> Users { get; set; }
}

而其生成的数据库操作上下文类的代码如下所示。

    public partial classDbEntities : DbContext
{
public DbEntities() : base("name=Model1")
{
}
public virtual DbSet<Role> Roles { get; set; }public virtual DbSet<User> Users { get; set; }public virtual DbSet<UserDetail> UserDetails { get; set; }protected override voidOnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Entity
<Role>()
.HasMany(e
=>e.Children)
.WithOptional(e
=>e.Parent)
.HasForeignKey(e
=>e.ParentID);

modelBuilder.Entity
<Role>()
.HasMany(e
=>e.Users)
.WithMany(e
=>e.Roles)
.Map(m
=> m.ToTable("UserRole"));

modelBuilder.Entity
<User>()
.HasMany(e
=>e.UserDetails)
.WithOptional(e
=>e.User)
.HasForeignKey(e
=>e.User_ID);

modelBuilder.Entity
<UserDetail>()
.Property(e
=>e.Height)
.HasPrecision(
18, 0);
}
}

2、使用Fluent API 配置的Code First模式代码结构

不管是Code First模式中使用 Fluent API 配置,还是使用了前面的Attribute特性标记的说明,都是为了从代码层面上构建实体类和表之间的信息,或者多个表之间一些关系,不过如果我们把这些实体类Attribute特性标记去掉的话,那么我们就可以通过Fluent API 配置进行属性和关系的指定了。

其实前面的OnModelCreating函数里面,已经使用了这种方式来配置表之间的关系了,为了纯粹使用Fluent API 配置,我们还需要把实体类进行简化,最终我们可以获得真正的实体类信息如下所示。

    public partial classUser
{
publicUser()
{
UserDetails
= new HashSet<UserDetail>();
Roles
= new HashSet<Role>();
}
public string ID { get; set; }public string Account { get; set; }public string Password { get; set; }public virtual ICollection<UserDetail> UserDetails { get; set; }public virtual ICollection<Role> Roles { get; set; }
}

这个实体类和我们以往的表现几乎一样,没有多余的信息,唯一多的就是完全是实体对象化了,包括了一些额外的关联对象信息。

前面说了,Oracle的生成实体类字段全部为大写字母,不过我们实体类还是需要保持它的Pascal模式书写格式,那么就可以在Fluent API 配置进行指定它的字段名为
大写
(注意,Oracle一定要指定字段名为大写,因为它是大小写敏感的)

最终我们定义了Oracle数据库USERS表对应映射关系如下所示。

    /// <summary>
    ///用户表USERS的映射信息(Fluent API 配置)/// </summary>
    public class UserMap : EntityTypeConfiguration<User>{publicUserMap()
{
HasMany(e
=> e.UserDetails).WithOptional(e => e.User).HasForeignKey(e =>e.User_ID);

Property(t
=> t.ID).HasColumnName("ID");
Property(t
=> t.Account).HasColumnName("ACCOUNT");
Property(t
=> t.Password).HasColumnName("PASSWORD");

ToTable(
"WHC.USERS");
}
}

我们为每一个字段进行了字段名称的映射,而且Oracle要大写,我们

通过
ToTable("WHC.USERS")
把它映射到了WHC.USERS表里面了。

如果对于有多对多中间表关系的Role来说,我们看看它的关系代码如下所示。

    /// <summary>
    ///用户表 ROLE 的映射信息(Fluent API 配置)/// </summary>
    public class RoleMap : EntityTypeConfiguration<Role>{publicRoleMap()
{
Property(t
=> t.ID).HasColumnName("ID");
Property(t
=> t.Name).HasColumnName("NAME");
Property(t
=> t.ParentID).HasColumnName("PARENTID");
ToTable(
"WHC.ROLE");

HasMany(e
=> e.Children).WithOptional(e => e.Parent).HasForeignKey(e =>e.ParentID);
HasMany(e
=> e.Users).WithMany(e => e.Roles).Map(m=>{
m.MapLeftKey(
"ROLE_ID");
m.MapRightKey(
"USER_ID");
m.ToTable(
"USERROLE", "WHC");
});
}
}

这里注意的是MapLeftKey和MapRightKey一定的对应好了,否则会有错误的问题,一般情况下,开始可能很难理解那个是Left,那个是Right,不过经过测试,可以发现Left的肯定是指向当前的这个映射实体的键(如上面的为ROLE_ID这个是Left一样,因为当前的实体映射是Role对象)。

通过这些映射代码的建立,我们为每个表都建立了一一的对应关系,剩下来的就是把这映射关系加载到数据库上下文对象里面了,还记得刚才说到的OnModelCreating吗,就是那里,一般我们加载的方式如下所示。

            //手工加载
            modelBuilder.Configurations.Add(newUserMap());
modelBuilder.Configurations.Add(
newRoleMap());
modelBuilder.Configurations.Add(
new UserDetailMap());

这种做法代替了原来的臃肿代码方式。

            modelBuilder.Entity<Role>()
.HasMany(e
=>e.Children)
.WithOptional(e
=>e.Parent)
.HasForeignKey(e
=>e.ParentID);

modelBuilder.Entity
<Role>()
.HasMany(e
=>e.Users)
.WithMany(e
=>e.Roles)
.Map(m
=> m.ToTable("UserRole"));

modelBuilder.Entity
<User>()
.HasMany(e
=>e.UserDetails)
.WithOptional(e
=>e.User)
.HasForeignKey(e
=>e.User_ID);

modelBuilder.Entity
<UserDetail>()
.Property(e
=>e.Height)
.HasPrecision(
18, 0);

一般情况下,到这里我认为基本上把整个思路已经介绍完毕了,不过精益求精一贯是个好事,对于上面的代码我还是觉得不够好,因为我每次在加载 Fluent API 配置的时候,都需要指定具体的映射类,非常不好,如果能够把它们动态加载进去,岂不妙哉。

对类似下面的关系硬编码可不是一件好事。

modelBuilder.Configurations.Add(newUserMap());
modelBuilder.Configurations.Add(
newRoleMap());
modelBuilder.Configurations.Add(
new UserDetailMap());

我们可以通过反射方式,把它们进行动态的加载即可。这样OnModelCreating函数处理的时候,就是很灵活的了,而且OnModelCreating函数只是在程序启动的时候映射一次而已,即使重复构建数据库操作上下文对象DbEntities的时候,也是不会重复触发这个OnModelCreating函数的,因此我们利用反射不会有后顾之忧,性能只是第一次慢一点而已,后面都不会重复触发了。

最终我们看看一步步下来的代码如下所示(注释的代码是不再使用的代码)。

        protected override voidOnModelCreating(DbModelBuilder modelBuilder)
{
#region MyRegion //modelBuilder.Entity<Role>()//.HasMany(e => e.Children)//.WithOptional(e => e.Parent)//.HasForeignKey(e => e.ParentID);//modelBuilder.Entity<Role>()//.HasMany(e => e.Users)//.WithMany(e => e.Roles)//.Map(m => m.ToTable("UserRole"));//modelBuilder.Entity<User>()//.HasMany(e => e.UserDetails)//.WithOptional(e => e.User)//.HasForeignKey(e => e.User_ID);//modelBuilder.Entity<UserDetail>()//.Property(e => e.Height)//.HasPrecision(18, 0);//手工加载//modelBuilder.Configurations.Add(new UserMap());//modelBuilder.Configurations.Add(new RoleMap());//modelBuilder.Configurations.Add(new UserDetailMap()); #endregion //使用数据库后缀命名,确保加载指定的数据库映射内容//string mapSuffix = ".Oracle";//.SqlServer/.Oracle/.MySql/.SQLite string mapSuffix =ConvertProviderNameToSuffix(defaultConnectStr.ProviderName);var typesToRegister =Assembly.GetExecutingAssembly().GetTypes()
.Where(type
=>type.Namespace.EndsWith(mapSuffix, StringComparison.OrdinalIgnoreCase))
.Where(type
=> !String.IsNullOrEmpty(type.Namespace))
.Where(type
=> type.BaseType != null &&type.BaseType.IsGenericType&& type.BaseType.GetGenericTypeDefinition() == typeof(EntityTypeConfiguration<>));foreach (var type intypesToRegister)
{
dynamic configurationInstance
=Activator.CreateInstance(type);
modelBuilder.Configurations.Add(configurationInstance);
}
base.OnModelCreating(modelBuilder);
}

这样我们运行程序运行正常,不在受约束于实体类的字段必须是大写的忧虑了。而且动态加载,对于我们使用其他数据库,依旧是个好事,因为其他数据库也只需要修改一下映射就可以了,真正远离了复杂的XML和实体类臃肿的Attribute书写内容,实现了非常弹性化的映射处理了。

最后我贴出一下测试的代码例子,和前面的随笔使用没有太大的差异。

        private void button1_Click(objectsender, EventArgs e)
{
DbEntities db
= newDbEntities();

User user
= newUser();
user.Account
= "TestName" +DateTime.Now.ToShortTimeString();
user.ID
=Guid.NewGuid().ToString();
user.Password
= "Test";

UserDetail detail
= new UserDetail() { ID = Guid.NewGuid().ToString(), Name = "userName33", Sex = 1, Note = "测试内容33", Height = 175};
user.UserDetails.Add(detail);
db.Users.Add(user);

Role role
= newRole();
role.ID
=Guid.NewGuid().ToString();
role.Name
= "TestRole";//role.Users.Add(user); user.Roles.Add(role);
db.Users.Add(user);
//db.Roles.Add(role); db.SaveChanges();

Role roleInfo
=db.Roles.FirstOrDefault();if (roleInfo != null)
{
Console.WriteLine(roleInfo.Name);
if (roleInfo.Users.Count > 0)
{
Console.WriteLine(roleInfo.Users.ToList()[
0].Account);
}
MessageBox.Show(
"OK");
}
}

测试Oracle数据库,我们可以发现数据添加到数据库里面了。

而且上面例子也创建了总结表的对应关系,具体数据如下所示。

如果是SQLServer,我们还可以看到数据库里面添加了一个额外的表,如下所示。

如果表的相关信息变化了,记得把这个表里面的记录清理一下,否则会出现一些错误提示,如果去找代码,可能会发现浪费很多时间都没有很好定位到具体的问题的。

这个表信息,在其它数据库里面没有发现,如Oracle、Mysql、Sqlite里面都没有,SQLServer这个表的具体数据如下所示。

整个项目的结构优化为标准的框架结构后,结构层次如下所示。