。
其实这段时间我并不是把这个系列给忘记了,而是一直在思考,想着接下来应该怎么写。因为园子里已经有很多非常优秀的EF的文章了,比如:
这些系列都写得非常好,基本涵盖了EF的所有常见技术点。我再详写这些就显得多余了,但为了整个系列的完整性,还是要提上一提,不然后面就没法继续了。
本篇会比较长,主要讲解UnitOfWork、Repository模式以及DbContext实现类的构造,来给架构demo添加数据访问功能,实现数据库验证的用户登录功能。
在数据库系统中,对于数据层来说,所有的操作归根结底无非“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性能不好的重要原因。
不知不觉说了那么多,呵呵,主要是关于数据查询有太多的话想说了,但这不是本文的重点,严重跑题了。直接上接口定义吧:
class="cnblogs_code_copy">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 }
实现类中所有操作最终都是通过单元操作来提交的,关于单元操作,马上就会讲到。
引入单元操作,主要是为了给各个实体维护一个共同的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> 49 public void Rollback() 50 { 51 IsCommitted = false; 52 } 53 54 public void Dispose() 55 { 56 if (!IsCommitted) 57 { 58 Commit(); 59 } 60 Context.Dispose(); 61 } 62 63 /// <summary> 64 /// 为指定的类型返回 System.Data.Entity.DbSet,这将允许对上下文中的给定实体执行 CRUD 操作。 65 /// </summary> 66 /// <typeparam name="TEntity"> 应为其返回一个集的实体类型。 </typeparam> 67 /// <returns> 给定实体类型的 System.Data.Entity.DbSet 实例。 </returns> 68 public DbSet<TEntity> Set<TEntity>() where TEntity : Entity 69 { 70 return Context.Set<TEntity>(); 71 } 72 73 /// <summary> 74 /// 注册一个新的对象到仓储上下文中 75 /// </summary> 76 /// <typeparam name="TEntity"> 要注册的类型 </typeparam> 77 /// <param name="entity"> 要注册的对象 </param> 78 public void RegisterNew<TEntity>(TEntity entity) where TEntity : Entity 79 { 80 EntityState state = Context.Entry(entity).State; 81 if (state == EntityState.Detached) 82 { 83 Context.Entry(entity).State = EntityState.Added; 84 } 85 IsCommitted = false; 86 } 87 88 /// <summary> 89 /// 批量注册多个新的对象到仓储上下文中 90 /// </summary> 91 /// <typeparam name="TEntity"> 要注册的类型 </typeparam> 92 /// <param name="entities"> 要注册的对象集合 </param> 93 public void RegisterNew<TEntity>(IEnumerable<TEntity> entities) where TEntity : Entity 94 { 95 try 96 { 97 Context.Configuration.AutoDetectChangesEnabled = false; 98 foreach (TEntity entity in entities) 99 { 100 RegisterNew(entity); 101 } 102 } 103 finally 104 { 105 Context.Configuration.AutoDetectChangesEnabled = true; 106 } 107 } 108 109 /// <summary> 110 /// 注册一个更改的对象到仓储上下文中 111 /// </summary> 112 /// <typeparam name="TEntity"> 要注册的类型 </typeparam> 113 /// <param name="entity"> 要注册的对象 </param> 114 public void RegisterModified<TEntity>(TEntity entity) where TEntity : Entity 115 { 116 if (Context.Entry(entity).State == EntityState.Detached) 117 { 118 Context.Set<TEntity>().Attach(entity); 119 } 120 Context.Entry(entity).State = EntityState.Modified; 121 IsCommitted = false; 122 } 123 124 /// <summary> 125 /// 注册一个删除的对象到仓储上下文中 126 /// </summary> 127 /// <typeparam name="TEntity"> 要注册的类型 </typeparam> 128 /// <param name="entity"> 要注册的对象 </param> 129 public void RegisterDeleted<TEntity>(TEntity entity) where TEntity : Entity 130 { 131 Context.Entry(entity).State = EntityState.Deleted; 132 IsCommitted = false; 133 } 134 135 /// <summary> 136 /// 批量注册多个删除的对象到仓储上下文中 137 /// </summary> 138 /// <typeparam name="TEntity"> 要注册的类型 </typeparam> 139 /// <param name="entities"> 要注册的对象集合 </param> 140 public void RegisterDeleted<TEntity>(IEnumerable<TEntity> entities) where TEntity : Entity 141 { 142 try 143 { 144 Context.Configuration.AutoDetectChangesEnabled = false; 145 foreach (TEntity entity in entities) 146 { 147 RegisterDeleted(entity); 148 } 149 } 150 finally 151 { 152 Context.Configuration.AutoDetectChangesEnabled = true; 153 } 154 } 155 } 156 }
实体数据操作中,需要特别说明一下的是批量操作。EF不支持批量操作(直接执行SQL语句的方式除外),但我们可以使用多次变更一次提交的方式来进行批量的插入,删除等操作。在进行数据的变更时,EF默认会自动的跟踪数据的变化(AutoDetectChangesEnabled = true),当变更的数据量较大的时候,EF的跟踪工作量就会骤增,使指定操作变得非常缓慢(这也是部分同学怀疑EF的性能问题的一个怀疑点),其实,只要在批量操作的时候把自动跟踪关闭(AutoDetectChangesEnabled = false),即可解决缓慢的问题。如以上代码 144 行与 152 行所示。
特别说明:本文的UnitOfWork实现方案参考了 dax.net 的 《深度剖析Byteart Retail案例:仓储(Repository)及其上下文(Repository Context)》,在此表示感谢。
至此,我们回顾一下解决方案的结构:
与业务实体无关的数据组件 GMF.Component.Data 已经搭建完成,下面的工作将与业务实体密切相关。
与业务实体相关的数据功能代码定义在 GMF.Demo.Core.Data 项目中。
首先,实现各个实体的 Repository 仓储操作,这里只以 用户信息(Member)实体为例:
1 namespace GMF.Demo.Core.Data.Repositories 2 { 3 /// <summary> 4 /// 仓储操作接口——用户信息 5 /// </summary> 6 public interface IMemberRepository : IRepository<Member> { } 7 }
1 namespace GMF.Demo.Core.Data.Repositories.Impl 2 { 3 /// <summary> 4 /// 仓储操作实现——用户信息 5 /// </summary> 6 [Export(typeof(IMemberRepository))] 7 public class MemberRepository : EFRepositoryBase<Member>, IMemberRepository { } 8 }
可以发现,通用仓储操作在数据组件中封装好后,在实际业务中只需要编写非常少量的代码即可实现各个实体的仓储操作,这就是封装的好处。
DbContext上下文的实现,这里先使用微软官方示例中的传统方案:
1 namespace GMF.Demo.Core.Data.Context 2 { 3 /// <summary> 4 /// Demo项目数据访问上下文 5 /// </summary> 6 [Export(typeof (DbContext))] 7 public class DemoDbContext : DbContext 8 { 9 #region 构造函数 10 11 /// <summary> 12 /// 初始化一个 使用连接名称为“default”的数据访问上下文类 的新实例 13 /// </summary> 14 public DemoDbContext() 15 : base("default") { } 16 17 /// <summary> 18 /// 初始化一个 使用指定数据连接名称或连接串 的数据访问上下文类 的新实例 19 /// </summary> 20 public DemoDbContext(string nameOrConnectionString) 21 : base(nameOrConnectionString) { } 22 23 #endregion 24 25 #region 属性 26 27 public DbSet<Role> Roles { get; set; } 28 29 public DbSet<Member> Members { get; set; } 30 31 public DbSet<MemberExtend> MemberExtends { get; set; } 32 33 public DbSet<LoginLog> LoginLogs { get; set; } 34 35 #endregion 36 37 protected override void OnModelCreating(DbModelBuilder modelBuilder) 38 { 39 //移除一对多的级联删除约定,想要级联删除可以在 EntityTypeConfiguration<TEntity>的实现类中进行控制 40 modelBuilder.Conventions.Remove<OneToManyCascadeDeleteConvention>(); 41 //多对多启用级联删除约定,不想级联删除可以在删除前判断关联的数据进行拦截 42 //modelBuilder.Conventions.Remove<ManyToManyCascadeDeleteConvention>(); 43 } 44 } 45 }
值得注意的是,在EF中提供了很多的规则来定义 EF生成数据库 的行为,如下图所示:
我们可以在 DbContext 的派生类中重写 OnModelCreating 方法来移除一些规则来达到某些需求,比如在此,我们移除 OneToManyCascadeDeleteConvention 来达到禁用数据库的 一对多的级联删除 ,需要时再在做实体映射时启用,就能防止由于误操作而导致实体相关的数据都被删除的情况。
当然,这里只定义的DbContext上下文的实现类,并没有进行使用,如何来使用这个上下文呢,只需实现一个单元操作实现类即可:
1 namespace GMF.Demo.Core.Data.Context 2 { 3 /// <summary> 4 /// Demo项目单元操作类 5 /// </summary> 6 [Export(typeof (IUnitOfWork))] 7 public class DemoUnitOfWorkContext : UnitOfWorkContextBase 8 { 9 /// <summary> 10 /// 获取或设置 当前使用的数据访问上下文对象 11 /// </summary> 12 protected override DbContext Context 13 { 14 get { return DemoDbContext; } 15 } 16 17 /// <summary> 18 /// 获取或设置 默认的Demo项目数据访问上下文对象 19 /// </summary> 20 [Import(typeof (DbContext))] 21 public DemoDbContext DemoDbContext { get; set; } 22 } 23 }
在本系列前面的示例中,由于没有数据访问功能,使用了一个数据来模拟数据源,现在,我们可以通过创建数据库时初始化一些数据,以使得在下面的操作中有数据可用。
EF创建数据库的策略同样有多种可选:
由各个策略类的名称即可猜到具体的作用(可见代码的语义化很重要),这里使用CreateDatabaseIfNotExists,即数据库不存在时创建。
1 namespace GMF.Demo.Core.Data.Initialize 2 { 3 /// <summary> 4 /// 数据库初始化策略 5 /// </summary> 6 public class SampleData : CreateDatabaseIfNotExists<DemoDbContext> 7 { 8 protected override void Seed(DemoDbContext context) 9 { 10 List<Member> members = new List<Member> 11 { 12 new Member { UserName = "admin", Password = "123456", Email = "admin@gmfcn.net", NickName = "管理员" }, 13 new Member { UserName = "gmfcn", Password = "123456", Email = "mf.guo@qq.com", NickName = "郭明锋" } 14 }; 15 DbSet<Member> memberSet = context.Set<Member>(); 16 members.ForEach(m => memberSet.Add(m)); 17 context.SaveChanges(); 18 } 19 } 20 }
为了不使业务代码与EF的耦合过大,我们规定:项目中对 EntityFramework.dll 的引用到 GMF.Demo.Core.Data项目止,再上层的代码不依赖于EF组件。因此还需要在这定义一个执行初始化的辅助类:
1 namespace GMF.Demo.Core.Data.Initialize 2 { 3 /// <summary> 4 /// 数据库初始化操作类 5 /// </summary> 6 public static class DatabaseInitializer 7 { 8 /// <summary> 9 /// 数据库初始化 10 /// </summary> 11 public static void Initialize( ) 12 { 13 Database.SetInitializer(new SampleData()); 14 using (var db = new DemoDbContext()) 15 { 16 db.Database.Initialize(false); 17 } 18 } 19 } 20 }
此辅助类将在程序初始化时调用,网站项目里,在Global的Application_Start方法中调用:
为了在业务实现中方便调用 IUnitOfWork 进行多实体的单元操作,定义一个核心与业务实现基类
1 namespace GMF.Demo.Core.Impl 2 { 3 /// <summary> 4 /// 核心业务实现基类 5 /// </summary> 6 public abstract class CoreServiceBase 7 { 8 /// <summary> 9 /// 获取或设置 工作单元对象,用于处理同步业务的事务操作 10 /// </summary> 11 [Import] 12 protected IUnitOfWork UnitOfWork { get; set; } 13 } 14 }
在核心业务实现中添加用到的各个实体Repository仓储操作接口对象的属性,并更新登录方法,使其调用仓储操作完成登录业务
1 namespace GMF.Demo.Core.Impl 2 { 3 /// <summary> 4 /// 账户模块核心业务实现 5 /// </summary> 6 public abstract class AccountService : CoreServiceBase, IAccountContract 7 { 8 #region 属性 9 10 #region 受保护的属性 11 12 /// <summary> 13 /// 获取或设置 用户信息数据访问对象 14 /// </summary> 15 [Import] 16 protected IMemberRepository MemberRepository { get; set; } 17 18 /// <summary> 19 /// 获取或设置 登录记录信息数据访问对象 20 /// </summary> 21 [Import] 22 protected ILoginLogRepository LoginLogRepository { get; set; } 23 24 #endregion 25 26 #endregion 27 28 /// <summary> 29 /// 用户登录 30 /// </summary> 31 /// <param name="loginInfo">登录信息</param> 32 /// <returns>业务操作结果</returns> 33 public virtual OperationResult Login(LoginInfo loginInfo) 34 { 35 PublicHelper.CheckArgument(loginInfo, "loginInfo"); 36 Member member = MemberRepository.Entities.SingleOrDefault(m => m.UserName == loginInfo.Access || m.Email == loginInfo.Access); 37 if (member == null) 38 { 39 return new OperationResult(OperationResultType.QueryNull, "指定账号的用户不存在。"); 40 } 41 if (member.Password != loginInfo.Password) 42 { 43 return new OperationResult(OperationResultType.Warning, "登录密码不正确。"); 44 } 45 LoginLog loginLog = new LoginLog { IpAddress = loginInfo.IpAddress, Member = member }; 46 LoginLogRepository.Insert(loginLog); 47 return new OperationResult(OperationResultType.Success, "登录成功。", member); 48 } 49 } 50 }
在此需要特别说明的是,为了保持项目结构的整洁与规范,实体数据操作的Repository与UnitOfWork只能被业务层(核心层及其派生层次)调用,而不能被业务层的上层调用,比如MVC项目中,不能被控制器调用,防止开发人员不遵守规范,在控制器中进行业务实现导致项目结构的混乱。因此给实体的Repository接口属性与业务基类中的UnitOfWork属性的可访问性都要设置为 protected,而不能为 public。
在MVC的Web.Config中添加一个数据连接配置:
<connectionStrings> <add name="default" connectionString="Data Source=.; Integrated Security=True; Initial Catalog=DemoContext; Pooling=True; MultipleActiveResultSets=True;" providerName="System.Data.SqlClient" /> </connectionStrings>
至此,我们的项目结构变成了如下图所示:
运行项目,尝试登录并成功,查看登录记录表,我们可以发现新增了一条登录记录:
GMFrameworkForBlog3.zip
为了让大家能第一时间获取到本架构的最新代码,也为了方便我对代码的管理,本系列的源码已加入微软的开源项目网站 http://www.codeplex.com,地址为:
https://gmframework.codeplex.com/