七色花权限管理系统(7)- 实现数据仓储和利用T4自动生成实体仓储
基于EntityFramework的数据访问层,我计划细分成数据核心、数据接口和数据实现。
其中数据核心已经在前几个章节中创建,那么在这篇日志里,将演示数据仓储(接口和实现)的实现及封装架构的整个过程。
仓储的作用
仓储的概念请自行搜索了解,我认为它最大的作用就是解耦。没有仓储,就只能直接使用EF数据库上下文对象来操作数据库,而为了“能使用EF数据库上下文对象来操作数据库(各实体库)”,就必须把实体关联给EF数据库上下文。例如MasterEntityContext中的DbSet属性:
1 /// <summary> 2 /// 用户 3 /// </summary> 4 public DbSet<SysUser> Users { get; set; } 5 6 /// <summary> 7 /// 角色 8 /// </summary> 9 public DbSet<SysRole> Roles { get; set; }
只有这样,我们才能在初始化EF数据库上下文对象之后,通过对象来访问相应的实体库,比如登录判定中那句代码:
1 var db = new MasterEntityContext("matrixkey"); 2 var entity = db.Users.Where(w => w.UserName == model.UserName).FirstOrDefault();
上述代码中的db.Users,其中Users对象,就是EF数据库上下文中定义的属性。
在WebUI层直接使用数据访问层的核心对象,这无疑是非常不正确的。在后面的日志中,我们将增加业务逻辑层,作为数据访问层和WebUI层的桥梁。
但是若没有仓储,即使有了业务逻辑层,仍然只能通过直接使用EF数据库上下文对象的方式来操作数据库,这就需要:
1、在业务层中暴露EF数据库上下文对象
2、在业务层中引用EntityFramework
这依旧很糟糕,有了EF数据库上下文对象,就等于有了一切。不能让业务层拥有一切,也不能让业务层引用EntityFramework。
于是就需要仓储作为“EF数据库上下文和业务层之间的桥梁”。对于业务层而言,仓储才是真正的数据访问层,业务层根本不知道EF数据库上下文的存在,它不关心数据访问层中究竟是谁提供了数据库访问的能力,无论是EF或是dapper或是源生的ADO.NET,它只关心数据访问层是否执行了它要操作的动作。
总之,业务层是无关数据驱动的,不能把EF暴露给业务层。仓储的实现,解耦了数据核心层和业务层,并且在下一章节中,还将解耦数据实体层和数据核心层。可见仓储最大的作用就是解耦。
用户仓储的简易实现
在解决方案下新建类库项目,名称S.Framework.DataAchieve。再创建相应的文件夹进行区分,结构如下图:
在Master下创建用户仓储类,名称SysUserRepository。上面说过,仓储是桥梁,是为其他需要获取数据的项目层提供代理服务的,那么在仓储类中,需要定义EF数据库上下文对象,以及通过该上下文对象对数据库的各种操作的方法的封装。
毫无疑问,数据实现层需要引用“数据实体层、数据核心层”。
这里以“用户登录功能”作为范例,一步一步演示用户仓储中的代码的演变:
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 using System.Threading.Tasks; 6 using System.Data.Entity; 7 using System.Data.Entity.Infrastructure; 8 9 using S.Framework.Entity.Master; 10 using S.Framework.DataCore.EntityFramework.EntityContexts; 11 12 namespace S.Framework.DataAchieve.EntityFramework.Repositories.Master 13 { 14 /// <summary> 15 /// 用户仓储 16 /// </summary> 17 public class SysUserRepository 18 { 19 20 } 21 } 22
定义数据库上下文对象:
1 /// <summary> 2 /// 数据库上下文 3 /// </summary> 4 private MasterEntityContext Db 5 { 6 get; 7 set; 8 }
数据库上下文对象作为最核心的内容,只允许在当前类中被设置和获取,因此该属性的访问修饰符设置为private。
定义用户仓储的构造方法,并在方法中初始化数据库上下文对象:
1 public SysUserRepository() 2 { 3 this.Db = new MasterEntityContext("matrixkey"); 4 }
“用户登录功能”中需要根据用户名获取用户实体,可以在仓储中定义该方法:
1 /// <summary> 2 /// 根据用户名获取用户实体 3 /// </summary> 4 /// <param name="userName">用户名</param> 5 /// <returns>用户实体</returns> 6 public SysUser GetByUserName(string userName) 7 { 8 return this.Db.Users.Where(w => w.UserName == userName).FirstOrDefault(); 9 }
理论上来说,用户仓储已经完成。
用户仓储功能检验结果
先在WebUI项目中增加对数据实现层(S.Framework.DataAchieve)的引用(对数据核心层的引用继续留着,因为数据库初始化策略的设置要用到核心层,等会会把数据库初始化设置从核心层移出去,那时再移除WebUI层对数据核心层的引用)。
把“用户登录功能”的代码调整为对用户仓储的调用,将
1 var db = new MasterEntityContext("matrixkey"); 2 var entity = db.Users.Where(w => w.UserName == model.UserName).FirstOrDefault();
修改为:
1 var rep = new SysUserRepository(); 2 var entity = rep.GetByUserName(model.UserName);
编译运行,用admin和123456进行登录,成功跳转至首页。
解耦数据核心层与WebUI层
上面说到,由于数据库初始化策略及其设置,都定义在数据核心层中,所以WebUI层需要依赖数据核心层。不能忍,必须解耦,方法是将数据库初始化策略及其设置类,移动到数据实现层中,如下图:
此时可以把数据核心层中EntityFramework文件夹下的Migrations、Initializes子文件夹删除。然后调整WebUI层中Global的命名空间即可。
编译,可能会有“找不到命名空间”的报错,原因是旧命名空间不存在了,删除这条using即可。
编译生成无误,然后移除WebUI层对数据核心层的引用吧。
由于数据库初始化类调整过,因此需要删除数据库让EF重新创建一次。删除数据库,重新去登录页面尝试登录。
仓储的完善
对实体的常用操作,可以简单地归纳为“特定查询、添加、更新、删除”,这几类操作是每一个实体仓储都需要包含的,因此可以通过封装来精简代码量。
建立一个仓储基本类,名称BaseRepository,将通用的部分在基本类中实现,如下:
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 using System.Threading.Tasks; 6 using System.Linq.Expressions; 7 8 using S.Framework.Entity; 9 using S.Framework.DataCore.EntityFramework.EntityContexts; 10 11 namespace S.Framework.DataAchieve.EntityFramework 12 { 13 public abstract class BaseRepository<TEntity> where TEntity : class, new() 14 { 15 private System.Data.Entity.DbContext Db { get; set; } 16 17 private System.Data.Entity.DbSet<TEntity> DbSet { get { return this.Db.Set<TEntity>(); } } 18 19 public BaseRepository() 20 { 21 this.Db = new MasterEntityContext("matrixkey"); 22 } 23 24 /// <summary> 25 /// 主键查询 26 /// </summary> 27 /// <param name="keyValues">键值</param> 28 /// <returns>实体</returns> 29 public virtual TEntity Find(IEnumerable<object> keyValues) 30 { 31 if (keyValues == null || keyValues.Count() == 0) 32 { 33 throw new ArgumentException("参数有误。"); 34 } 35 keyValues = keyValues.Where(keyValue => keyValue != null); 36 return this.DbSet.Find(keyValues); 37 } 38 39 /// <summary> 40 /// 主键查询 41 /// </summary> 42 /// <param name="keyValues">键值</param> 43 /// <returns>实体</returns> 44 public virtual TEntity Find(params object[] keyValues) 45 { 46 if (keyValues == null || keyValues.Count() == 0) 47 { 48 throw new ArgumentException("参数有误。"); 49 } 50 51 return this.DbSet.Find(keyValues); 52 } 53 54 /// <summary> 55 /// 获取 <see cref="TEntity"/> 的Linq查询器 56 /// </summary> 57 /// <returns></returns> 58 protected IQueryable<TEntity> Query() 59 { 60 return this.DbSet.AsQueryable(); 61 } 62 63 /// <summary> 64 /// 获取 <see cref="TEntity"/> 的Linq查询器 65 /// </summary> 66 /// <param name="predicate">查询条件</param> 67 /// <returns>数据查询器</returns> 68 protected IQueryable<TEntity> Query(Expression<Func<TEntity, bool>> predicate) 69 { 70 return this.DbSet.Where(predicate); 71 } 72 73 /// <summary> 74 /// 添加实体 75 /// </summary> 76 /// <param name="entity">实体</param> 77 public void Add(TEntity entity) 78 { 79 if (entity == null) 80 { return; } 81 82 this.DbSet.Add(entity); 83 } 84 85 /// <summary> 86 /// 批量添加实体 87 /// </summary> 88 /// <param name="entities">实体集合</param> 89 public void AddRange(IEnumerable<TEntity> entities) 90 { 91 if (entities == null || entities.Count() == 0) 92 { return; } 93 94 System.Data.Entity.DbSet<TEntity> set = this.DbSet; 95 bool autoDetectChangesEnabled = this.Db.Configuration.AutoDetectChangesEnabled; 96 this.Db.Configuration.AutoDetectChangesEnabled = false; 97 98 foreach (TEntity entity in entities) 99 { 100 if (entity == null) 101 { continue; } 102 103 set.Add(entity); 104 } 105 106 this.Db.Configuration.AutoDetectChangesEnabled = autoDetectChangesEnabled; 107 } 108 109 /// <summary> 110 /// 更改实体 111 /// </summary> 112 /// <param name="entity">实体对象</param> 113 public void Update(TEntity entity) 114 { 115 if (entity == null) 116 { return; } 117 118 System.Data.Entity.Infrastructure.DbEntityEntry<TEntity> entry = this.Db.Entry(entity); 119 if (entry.State == System.Data.Entity.EntityState.Detached) 120 { 121 this.DbSet.Attach(entity); 122 } 123 entry.State = System.Data.Entity.EntityState.Modified; 124 } 125 126 /// <summary> 127 /// 批量更改实体 128 /// </summary> 129 /// <param name="entities">实体集合</param> 130 public void UpdateRange(IEnumerable<TEntity> entities) 131 { 132 if (entities == null || entities.Count() == 0) 133 { return; } 134 135 var set = this.DbSet; 136 bool autoDetectChangesEnabled = this.Db.Configuration.AutoDetectChangesEnabled; 137 this.Db.Configuration.AutoDetectChangesEnabled = false; 138 139 foreach (TEntity entity in entities) 140 { 141 if (entity == null) 142 { continue; } 143 144 System.Data.Entity.Infrastructure.DbEntityEntry<TEntity> entry = this.Db.Entry(entity); 145 if (entry.State == System.Data.Entity.EntityState.Detached) 146 { 147 set.Attach(entity); 148 } 149 entry.State = System.Data.Entity.EntityState.Modified; 150 } 151 152 this.Db.Configuration.AutoDetectChangesEnabled = autoDetectChangesEnabled; 153 } 154 155 /// <summary> 156 /// 主键删除实体 157 /// </summary> 158 /// <param name="key">键值</param> 159 public void Delete(object key) 160 { 161 if (key == null || string.IsNullOrWhiteSpace(key.ToString())) 162 { return; } 163 164 TEntity entity = this.Find(key); 165 166 this.Delete(entity); 167 } 168 169 /// <summary> 170 /// 删除实体 171 /// </summary> 172 /// <param name="entity">实体</param> 173 public void Delete(TEntity entity) 174 { 175 if (entity == null) 176 { return; } 177 178 System.Data.Entity.DbSet<TEntity> set = this.DbSet; 179 System.Data.Entity.Infrastructure.DbEntityEntry<TEntity> entry = this.Db.Entry(entity); 180 if (entry.State == System.Data.Entity.EntityState.Detached) 181 { 182 set.Attach(entity); set.Remove(entity); 183 } 184 else 185 { entry.State = System.Data.Entity.EntityState.Deleted; } 186 } 187 188 /// <summary> 189 /// 批量删除实体 190 /// </summary> 191 /// <param name="entities">实体集合</param> 192 public void DeleteRange(IEnumerable<TEntity> entities) 193 { 194 if (entities == null || entities.Count() == 0) 195 { return; } 196 var set = this.DbSet; 197 198 bool autoDetectChangesEnabled = this.Db.Configuration.AutoDetectChangesEnabled; 199 this.Db.Configuration.AutoDetectChangesEnabled = false; 200 201 foreach (TEntity entity in entities) 202 { 203 if (entity == null) 204 { continue; } 205 206 System.Data.Entity.Infrastructure.DbEntityEntry<TEntity> entry = this.Db.Entry(entity); 207 if (entry.State == System.Data.Entity.EntityState.Detached) 208 { 209 set.Attach(entity); 210 } 211 else 212 { 213 entry.State = System.Data.Entity.EntityState.Deleted; 214 } 215 } 216 217 this.Db.Configuration.AutoDetectChangesEnabled = autoDetectChangesEnabled; 218 } 219 } 220 } 221
注意仓储基本类的修饰符以及类中各属性、方法等成员的访问修饰符,要充分理解abstract、private、protected、public的使用原因。不了解泛型的读者请自行查阅相关资料。
现在让用户仓储类继承仓储基本类,并删除“在父类中已经实现的成员几相关代码”,用户仓储类将变得非常简单:
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 using System.Threading.Tasks; 6 7 using S.Framework.Entity.Master; 8 9 namespace S.Framework.DataAchieve.EntityFramework.Repositories.Master 10 { 11 /// <summary> 12 /// 用户仓储 13 /// </summary> 14 public class SysUserRepository : BaseRepository<SysUser> 15 { 16 /// <summary> 17 /// 根据用户名获取用户实体 18 /// </summary> 19 /// <param name="userName">用户名</param> 20 /// <returns>用户实体</returns> 21 public SysUser GetByUserName(string userName) 22 { 23 return this.Query(w => w.UserName == userName).FirstOrDefault(); 24 } 25 } 26 } 27
编译运行,再次进行登录,成功跳转至首页。
现在为角色实体创建仓储类试试,简单吧。
但同时也引出新问题:手动为每个实体创建实体仓储是个重复又机械的体力活,是否可以像“实体配置类”那样也用T4模板来自动生成?
当然可以。过程和“利用T4自动生成实体配置类”一样,模板不同而已,这里一笔带过,直接贴出“仓储模板”和“执行器”的相关代码:
1 <#+ 2 // <copyright file="Repository.tt" company=""> 3 // Copyright © . All Rights Reserved. 4 // </copyright> 5 6 public class Repository : CSharpTemplate 7 { 8 private string _modelName; 9 private string _prefixName; 10 11 public Repository(string modelName, string prefixName) 12 { 13 this._modelName = modelName; 14 this._prefixName = prefixName; 15 } 16 public override string TransformText() 17 { 18 base.TransformText(); 19 #> 20 using System; 21 using System.Collections.Generic; 22 using System.Linq; 23 using System.Text; 24 using System.Threading.Tasks; 25 26 using S.Framework.Entity.<#= _prefixName #>; 27 28 namespace S.Framework.DataAchieve.EntityFramework.Repositories.<#= _prefixName #> 29 { 30 /// <summary> 31 /// 实体仓储 32 /// </summary> 33 public partial class <#= _modelName #>Repository : BaseRepository<<#= _modelName #>> 34 { 35 36 } 37 } 38 <#+ 39 return this.GenerationEnvironment.ToString(); 40 } 41 } 42 #> 43
1 <#@ template language="C#" debug="True" #> 2 <#@ assembly name="System.Core" #> 3 <#@ output extension="cs" #> 4 <#@ import namespace="System.IO" #> 5 <#@ import namespace="System.Text" #> 6 <#@ import namespace="System.Reflection" #> 7 <#@ import namespace="System.Linq" #> 8 <#@ import namespace="System.Collections.Generic" #> 9 <#@ include file="T4Toolbox.tt" #> 10 <#@ include file="Repository.tt" #> 11 <# 12 13 string coreName = "S.Framework", projectName = coreName + ".DataAchieve", entityProjectName = coreName + ".Entity"; 14 string entityBaseModelName = entityProjectName + ".EntityBaseModel"; 15 string entityBaseModelNameForReflection = entityProjectName + ".EntityModelBaseForReflection"; 16 //当前完整路径 17 string currentPath = Path.GetDirectoryName(Host.TemplateFile); 18 //T4文件夹的父级文件夹路径 19 string projectPath = currentPath.Substring(0, currentPath.IndexOf(@"\T4")); 20 //解决方案路径 21 string solutionFolderPath = currentPath.Substring(0, currentPath.IndexOf(@"\" + projectName)); 22 23 //加载数据实体.dll 24 string entityFilePath = string.Concat(solutionFolderPath, ("\\"+ entityProjectName +"\\bin\\Debug\\" + entityProjectName + ".dll")); 25 byte[] fileData = File.ReadAllBytes(entityFilePath); 26 Assembly assembly = Assembly.Load(fileData); 27 //反射出实体类,不知道为啥此处不能成功判定“是否继承EntityModelBaseForReflection类” 28 //因此只能通过名称比较的方式来判定 29 IEnumerable<Type> modelTypes = assembly.GetTypes().Where(m => m.IsClass && !m.IsAbstract && (m.BaseType.FullName.Equals(entityBaseModelName) || m.BaseType.FullName.Equals(entityBaseModelNameForReflection))); 30 31 //循环实体类 32 List<string> prefixNames = new List<string>(); 33 foreach (Type item in modelTypes) 34 { 35 //找 实体文件夹 名称 36 string tempNamespace= item.Namespace, nameSpaceWithoutProjectName = tempNamespace.Substring(entityProjectName.Length); 37 if(nameSpaceWithoutProjectName.IndexOf(".") != 0 || nameSpaceWithoutProjectName.LastIndexOf(".") > 0) 38 { continue; } 39 40 //是否直接继承实体基本类 41 bool purity = item.BaseType.FullName.Equals(entityBaseModelNameForReflection); 42 //实体所在的数据库标识名称 43 string targetName = nameSpaceWithoutProjectName.Substring(1); 44 if(!prefixNames.Any(a => a == targetName)){ prefixNames.Add(targetName); } 45 //目标文件的路径和名称(嵌套Generate文件夹是为了标识T4生成的类文件) 46 string fileName= targetName + @"\Generate\" + item.Name + "Repository.cs"; 47 48 //仓储文件 49 string folderName= @"\Repositories\"; 50 Repository repository = new Repository(item.Name, targetName); 51 repository.Output.Encoding = Encoding.UTF8; 52 string path = projectPath + folderName + fileName; 53 repository.RenderToFile(path); 54 } 55 #> 56
通过T4模板生成的实体仓储文件,结构如下图:
下一篇日志,将演示数据实体层和数据核心层的解耦。
截止本章节,项目源码下载:点击下载(存在百度云盘中)