〇、目录
一、前言
四、业务整合应用
(一) 业务实体的仓储实现
(二) 数据上下文实现
(三) 创建数据库时的数据初始化
(四) 业务整合,实现登录功能
五、源码获取
六、系列导航
一、前言
终于到EF了,实在不好意思,最近有点忙,本篇离上一篇发布已经一个多星期了,工作中的小迭代告一段落,终于有点时间来继续我们的架构设计了,在这里先对大家表示歉意。
其实这段时间我并不是把这个系列给忘记了,而是一直在思考,想着接下来应该怎么写。因为园子里已经有很多非常优秀的EF的文章了,比如:
- Entity Framework Code First 学习日记
- 【译著】Code First :使用Entity. Framework编程
- Entity Framework技术系列
- EF框架step by step
这些系列都写得非常好,基本涵盖了EF的所有常见技术点。我再详写这些就显得多余了,但为了整个系列的完整性,还是要提上一提,不然后面就没法继续了。
本篇会比较长,主要讲解UnitOfWork、Repository模式以及DbContext实现类的构造,来给架构demo添加数据访问功能,实现数据库验证的用户登录功能。
二、Repository
在数据库系统中,对于数据层来说,所有的操作归根结底无非“C(增加)、R(读取)、U(修改)、D(删除)”这四种操作。四种操作当中,与与业务相关度最大的是读取操作,根据各种不同的业务需求提交不同的查询,其最终执行应该放到业务层面中去进行,而增加,修改,删除这三种操作较为通用,可以作为通用数据操作封装到Repository中。在Repository中,唯一的变化点就是各种不同的实体类型,既然是变化点就应该进行封装,这里使用泛型来封装这个变化点。
对于实体的查询操作,需要着重说明一下。EF是ORM,ORM最大的特点就是看到的使用的都是对象,读取一个对象,就相当于执行了一个“select * from table”的查询操作,稍微了解数据库的同学都知道,这是非常消耗性能与内存的,数据查询最好还是应该按需查询,要什么取什么。EF的查询操作如果不处理好,很容易造成巨大的性能问题,比如有些同学喜欢在Repository中定义诸如GetByName,GetByXX,GetByXXX的操作,这些操作通常并不是每个实体都需要,即使需要也很有局限性,最后这些操作大都伦为“鸡肋”,限制了使用场景,也不能按需查询。还有些同学定义考虑到了按条件查询,定义了比如GetByPredicate(predicate)的操作,但使用了IEnumerable<T>的返回值,相当于把这部分数据都加载到内存中,再进行后续操作。甚至还有定义 IEnumerable<T> GetAll(); 操作的,明白的同学都知道这是件多么恐怖的事,相当于把整个表的数据都加载到内存中,再进行后续操作!!!诸如此类,问题多多……其实,数据查询,只要在Repository中定义一个只读的IQueryable<T>的数据集,其他的事就完全不用管了,业务层中想要什么就取什么就好了,当然,为了充分利用EF对主键查询的缓存特性,定义一个 GetByKey 的查询操作还是可以接受的。PS:关于EF数据查询的问题,后面还会专门有一篇文章来探讨。总之,EF的数据查询做不好,影响很大,这也是很多同学说EF性能不好的重要原因。
不知不觉说了那么多,呵呵,主要是关于数据查询有太多的话想说了,但这不是本文的重点,严重跑题了。直接上接口定义吧:
1 namespace GMF.Component.Data 2 { 3 /// <summary> 4 /// 定义仓储模型中的数据标准操作 5 /// </summary> 6 /// <typeparam name="TEntity">动态实体类型</typeparam> 7 public interface IRepository<TEntity> where TEntity : Entity 8 { 9 #region 属性 10 11 /// <summary> 12 /// 获取 当前实体的查询数据集 13 /// </summary> 14 IQueryable<TEntity> Entities { get; } 15 16 #endregion 17 18 #region 公共方法 19 20 /// <summary> 21 /// 插入实体记录 22 /// </summary> 23 /// <param name="entity"> 实体对象 </param> 24 /// <param name="isSave"> 是否执行保存 </param> 25 /// <returns> 操作影响的行数 </returns> 26 int Insert(TEntity entity, bool isSave = true); 27 28 /// <summary> 29 /// 批量插入实体记录集合 30 /// </summary> 31 /// <param name="entities"> 实体记录集合 </param> 32 /// <param name="isSave"> 是否执行保存 </param> 33 /// <returns> 操作影响的行数 </returns> 34 int Insert(IEnumerable<TEntity> entities, bool isSave = true); 35 36 /// <summary> 37 /// 删除指定编号的记录 38 /// </summary> 39 /// <param name="id"> 实体记录编号 </param> 40 /// <param name="isSave"> 是否执行保存 </param> 41 /// <returns> 操作影响的行数 </returns> 42 int Delete(object id, bool isSave = true); 43 44 /// <summary> 45 /// 删除实体记录 46 /// </summary> 47 /// <param name="entity"> 实体对象 </param> 48 /// <param name="isSave"> 是否执行保存 </param> 49 /// <returns> 操作影响的行数 </returns> 50 int Delete(TEntity entity, bool isSave = true); 51 52 /// <summary> 53 /// 删除实体记录集合 54 /// </summary> 55 /// <param name="entities"> 实体记录集合 </param> 56 /// <param name="isSave"> 是否执行保存 </param> 57 /// <returns> 操作影响的行数 </returns> 58 int Delete(IEnumerable<TEntity> entities, bool isSave = true); 59 60 /// <summary> 61 /// 删除所有符合特定表达式的数据 62 /// </summary> 63 /// <param name="predicate"> 查询条件谓语表达式 </param> 64 /// <param name="isSave"> 是否执行保存 </param> 65 /// <returns> 操作影响的行数 </returns> 66 int Delete(Expression<Func<TEntity, bool>> predicate, bool isSave = true); 67 68 /// <summary> 69 /// 更新实体记录 70 /// </summary> 71 /// <param name="entity"> 实体对象 </param> 72 /// <param name="isSave"> 是否执行保存 </param> 73 /// <returns> 操作影响的行数 </returns> 74 int Update(TEntity entity, bool isSave = true); 75 76 /// <summary> 77 /// 查找指定主键的实体记录 78 /// </summary> 79 /// <param name="key"> 指定主键 </param> 80 /// <returns> 符合编号的记录,不存在返回null </returns> 81 TEntity GetByKey(object key); 82 83 #endregion 84 } 85 }
还要说明一下,每个操作方法都带有一个 isSave 可选参数,是为了单个实体操作的需要,免去了每次都要调用 context.SaveChanged()的麻烦。如果是进行多个实体的单元事务操作,就需要把这个参数设置为 false 。
Repository的通用实现如下:
1 namespace GMF.Component.Data 2 { 3 /// <summary> 4 /// EntityFramework仓储操作基类 5 /// </summary> 6 /// <typeparam name="TEntity">动态实体类型</typeparam> 7 public abstract class EFRepositoryBase<TEntity> : IRepository<TEntity> where TEntity : Entity 8 { 9 #region 属性 10 11 /// <summary> 12 /// 获取 仓储上下文的实例 13 /// </summary> 14 [Import] 15 public IUnitOfWork UnitOfWork { get; set; } 16 17 /// <summary> 18 /// 获取或设置 EntityFramework的数据仓储上下文 19 /// </summary> 20 protected IUnitOfWorkContext EFContext 21 { 22 get 23 { 24 if (UnitOfWork is IUnitOfWorkContext) 25 { 26 return UnitOfWork as IUnitOfWorkContext; 27 } 28 throw new DataAccessException(string.Format("数据仓储上下文对象类型不正确,应为IUnitOfWorkContext,实际为 {0}", UnitOfWork.GetType().Name)); 29 } 30 } 31 32 /// <summary> 33 /// 获取 当前实体的查询数据集 34 /// </summary> 35 public virtual IQueryable<TEntity> Entities 36 { 37 get { return EFContext.Set<TEntity>(); } 38 } 39 40 #endregion 41 42 #region 公共方法 43 44 /// <summary> 45 /// 插入实体记录 46 /// </summary> 47 /// <param name="entity"> 实体对象 </param> 48 /// <param name="isSave"> 是否执行保存 </param> 49 /// <returns> 操作影响的行数 </returns> 50 public virtual int Insert(TEntity entity, bool isSave = true) 51 { 52 PublicHelper.CheckArgument(entity, "entity"); 53 EFContext.RegisterNew(entity); 54 return isSave ? EFContext.Commit() : 0; 55 } 56 57 /// <summary> 58 /// 批量插入实体记录集合 59 /// </summary> 60 /// <param name="entities"> 实体记录集合 </param> 61 /// <param name="isSave"> 是否执行保存 </param> 62 /// <returns> 操作影响的行数 </returns> 63 public virtual int Insert(IEnumerable<TEntity> entities, bool isSave = true) 64 { 65 PublicHelper.CheckArgument(entities, "entities"); 66 EFContext.RegisterNew(entities); 67 return isSave ? EFContext.Commit() : 0; 68 } 69 70 /// <summary> 71 /// 删除指定编号的记录 72 /// </summary> 73 /// <param name="id"> 实体记录编号 </param> 74 /// <param name="isSave"> 是否执行保存 </param> 75 /// <returns> 操作影响的行数 </returns> 76 public virtual int Delete(object id, bool isSave = true) 77 { 78 PublicHelper.CheckArgument(id, "id"); 79 TEntity entity = EFContext.Set<TEntity>().Find(id); 80 return entity != null ? Delete(entity, isSave) : 0; 81 } 82 83 /// <summary> 84 /// 删除实体记录 85 /// </summary> 86 /// <param name="entity"> 实体对象 </param> 87 /// <param name="isSave"> 是否执行保存 </param> 88 /// <returns> 操作影响的行数 </returns> 89 public virtual int Delete(TEntity entity, bool isSave = true) 90 { 91 PublicHelper.CheckArgument(entity, "entity"); 92 EFContext.RegisterDeleted(entity); 93 return isSave ? EFContext.Commit() : 0; 94 } 95 96 /// <summary> 97 /// 删除实体记录集合 98 /// </summary> 99 /// <param name="entities"> 实体记录集合 </param> 100 /// <param name="isSave"> 是否执行保存 </param> 101 /// <returns> 操作影响的行数 </returns> 102 public virtual int Delete(IEnumerable<TEntity> entities, bool isSave = true) 103 { 104 PublicHelper.CheckArgument(entities, "entities"); 105 EFContext.RegisterDeleted(entities); 106 return isSave ? EFContext.Commit() : 0; 107 } 108 109 /// <summary> 110 /// 删除所有符合特定表达式的数据 111 /// </summary> 112 /// <param name="predicate"> 查询条件谓语表达式 </param> 113 /// <param name="isSave"> 是否执行保存 </param> 114 /// <returns> 操作影响的行数 </returns> 115 public virtual int Delete(Expression<Func<TEntity, bool>> predicate, bool isSave = true) 116 { 117 PublicHelper.CheckArgument(predicate, "predicate"); 118 List<TEntity> entities = EFContext.Set<TEntity>().Where(predicate).ToList; 119 return entities.Count > 0 ? Delete(entities, isSave) : 0; 120 } 121 122 /// <summary> 123 /// 更新实体记录 124 /// </summary> 125 /// <param name="entity"> 实体对象 </param> 126 /// <param name="isSave"> 是否执行保存 </param> 127 /// <returns> 操作影响的行数 </returns> 128 public virtual int Update(TEntity entity, bool isSave = true) 129 { 130 PublicHelper.CheckArgument(entity, "entity"); 131 EFContext.RegisterModified(entity); 132 return isSave ? EFContext.Commit() : 0; 133 } 134 135 /// <summary> 136 /// 查找指定主键的实体记录 137 /// </summary> 138 /// <param name="key"> 指定主键 </param> 139 /// <returns> 符合编号的记录,不存在返回null </returns> 140 public virtual TEntity GetByKey(object key) 141 { 142 PublicHelper.CheckArgument(key, "key"); 143 return EFContext.Set<TEntity>().Find(key); 144 } 145 146 #endregion 147 } 148 }
实现类中所有操作最终都是通过单元操作来提交的,关于单元操作,马上就会讲到。
三、UnitOfWork
引入单元操作,主要是为了给各个实体维护一个共同的DbContext上下文对象,保证所有的操作都是在共同的上下文中进行的。EF的操作提交 context.SaveChanged() 默认就是事务性的,只要保证了当前的所有实体的操作都是在一个共同的上下文中进行的,就实现了事务操作了。
在业务层中,各个实体的增删改操作都是通过各个实体的Repository进行的,只需要提供一个提交保存的功能作为最后调用,即可保证当前的提交是事务性的。因此定义给业务层引用的单元操作接口如下:
1 namespace GMF.Component.Data 2 { 3 /// <summary> 4 /// 业务单元操作接口 5 /// </summary> 6 public interface IUnitOfWork 7 { 8 #region 属性 9 10 /// <summary> 11 /// 获取 当前单元操作是否已被提交 12 /// </summary> 13 bool IsCommitted { get; } 14 15 #endregion 16 17 #region 方法 18 19 /// <summary> 20 /// 提交当前单元操作的结果 21 /// </summary> 22 /// <returns></returns> 23 int Commit(); 24 25 /// <summary> 26 /// 把当前单元操作回滚成未提交状态 27 /// </summary> 28 void Rollback(); 29 30 #endregion 31 } 32 }
在数据组件内部,数据操作最终都提交到一个与IUnitOfWork接口的实现类中进行操作,以保证各个实体的Repository与IUnitOfWork使用的是同一个DbContext上下文。定义数据单元操作文接口如下:
1 namespace GMF.Component.Data 2 { 3 /// <summary> 4 /// 数据单元操作接口 5 /// </summary> 6 public interface IUnitOfWorkContext : IUnitOfWork, IDisposable 7 { 8 /// <summary> 9 /// 为指定的类型返回 System.Data.Entity.DbSet,这将允许对上下文中的给定实体执行 CRUD 操作。 10 /// </summary> 11 /// <typeparam name="TEntity"> 应为其返回一个集的实体类型。 </typeparam> 12 /// <returns> 给定实体类型的 System.Data.Entity.DbSet 实例。 </returns> 13 DbSet<TEntity> Set<TEntity>() where TEntity : Entity; 14 15 /// <summary> 16 /// 注册一个新的对象到仓储上下文中 17 /// </summary> 18 /// <typeparam name="TEntity"> 要注册的类型 </typeparam> 19 /// <param name="entity"> 要注册的对象 </param> 20 void RegisterNew<TEntity>(TEntity entity) where TEntity : Entity; 21 22 /// <summary> 23 /// 批量注册多个新的对象到仓储上下文中 24 /// </summary> 25 /// <typeparam name="TEntity"> 要注册的类型 </typeparam> 26 /// <param name="entities"> 要注册的对象集合 </param> 27 void RegisterNew<TEntity>(IEnumerable<TEntity> entities) where TEntity : Entity; 28 29 /// <summary> 30 /// 注册一个更改的对象到仓储上下文中 31 /// </summary> 32 /// <typeparam name="TEntity"> 要注册的类型 </typeparam> 33 /// <param name="entity"> 要注册的对象 </param> 34 void RegisterModified<TEntity>(TEntity entity) where TEntity : Entity; 35 36 /// <summary> 37 /// 注册一个删除的对象到仓储上下文中 38 /// </summary> 39 /// <typeparam name="TEntity"> 要注册的类型 </typeparam> 40 /// <param name="entity"> 要注册的对象 </param> 41 void RegisterDeleted<TEntity>(TEntity entity) where TEntity : Entity; 42 43 /// <summary> 44 /// 批量注册多个删除的对象到仓储上下文中 45 /// </summary> 46 /// <typeparam name="TEntity"> 要注册的类型 </typeparam> 47 /// <param name="entities"> 要注册的对象集合 </param> 48 void RegisterDeleted<TEntity>(IEnumerable<TEntity> entities) where TEntity : Entity; 49 } 50 }
在单元操作的实现基类中,定义一个只读的DbContext抽象属性,实际的DbContext上下文需要在实现类中进行重写赋值。
1 namespace GMF.Component.Data
2 {
3 /// <summary>
4 /// 单元操作实现
5 /// </summary>
6 public abstract class UnitOfWorkContextBase : IUnitOfWorkContext
7 {
8 /// <summary>
9 /// 获取 当前使用的数据访问上下文对象
10 /// </summary>
11 protected abstract DbContext Context { get; }
12
13 /// <summary>
14 /// 获取 当前单元操作是否已被提交
15 /// </summary>
16 public bool IsCommitted { get; private set; }
17
18 /// <summary>
19 /// 提交当前单元操作的结果
20 /// </summary>
21 /// <returns></returns>
22 public int Commit()
23 {
24 if (IsCommitted)
25 {
26 return 0;
27 }
28 try
29 {
30 int result = Context.SaveChanges();
31 IsCommitted = true;
32 return result;
33 }
34 catch (DbUpdateException e)
35 {
36 if (e.InnerException != null && e.InnerException.InnerException is SqlException)
37 {
38 SqlException sqlEx = e.InnerException.InnerException as SqlException;
39 string msg = DataHelper.GetSqlExceptionMessage(sqlEx.Number);
40 throw PublicHelper.ThrowDataAccessException("提交数据更新时发生异常:" + msg, sqlEx);
41 }
42 throw;
43 }
44 }
45
46 /// <summary>
47 /// 把当前单元操作回滚成未提交状态
48 /// </summary>