经过前面的工作,系统正变得越来越清晰。
现在有一个问题需要解决。当需要额外增加一个数据表时,我们需要做的操作是:
在实体层创建实体并编译实体层
在核心层运行T4
配置实体
将实体对象关联给EF数据库上下文(定义DbSet)
将实体配置注册给EF配置对象
这过于繁琐,最后2个步骤,强行地把实体关联在EF数据库上下文里,导致了两者的耦合。这篇日志将演示如何将最后2个步骤省略,解放EF数据库上下文,不用手动关联实体对象,不用手动注册实体配置。
回顾和解释
最后两步还记得吗?
通过定义DbSet对象将实体关联给EF:
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; }
那么定义DbSet究竟是用来干什么的?
它有2个作用:
在没有注册实体配置的情况下,告诉EF,要把指定实体映射到数据库(如果注册了实体配置,就起不到这个作用了)
能够显式地通过db对象访问实体库(db.Uses)
在第5章节中,我们加入了实体配置,因此第1个中作用就无效了。那来看看第2个作用能否取缔掉,如果能够取缔掉,那就有了“解耦”的可能。
回顾用户登录功能的最初代码:
1 db.Users.Where(w => w.UserName == model.UserName).FirstOrDefault()
当时还没有建立仓储,于是直接用EF数据库上下文对象访问用户库(db.Users)。
后来建立了仓储,代码修改为:
1 this.Query(w => w.UserName == userName).FirstOrDefault()
1 /// <summary> 2 /// 获取 <see cref="TEntity"/> 的Linq查询器 3 /// </summary> 4 /// <param name="predicate">查询条件</param> 5 /// <returns>数据查询器</returns> 6 protected IQueryable<TEntity> Query(Expression<Func<TEntity, bool>> predicate) 7 { 8 return this.DbSet.Where(predicate); 9 }
1 private System.Data.Entity.DbSet<TEntity> DbSet { get { return this.Db.Set<TEntity>(); } }
可以看到,修改后的代码,并未调用db.Users,而是调用db.Set<SysUser>()。
回顾数据初始化策略中关于自动生成数据的代码:
1 /// <summary> 2 /// 数据初始化 3 /// </summary> 4 /// <param name="context">数据库上下文</param> 5 protected override void Seed(MasterEntityContext context) 6 { 7 if (!context.Users.Any(a => a.UserName == "admin")) 8 { 9 var entity = new SysUser { ID = Guid.NewGuid().ToString(), UserName = "admin", Password = "123456", CreateDate = DateTime.Now, CreateUser = "admin" }; 10 //将用户对象附加给数据库上下文(这仅仅是内存级别的操作) 11 context.Users.Add(entity); 12 //数据库上下文保存更改(提交变更到数据库执行) 13 context.SaveChanges(); 14 } 15 }
这里的db.Users同样也可以替换掉,修改代码后如下:
1 /// <summary> 2 /// 数据初始化 3 /// </summary> 4 /// <param name="context">数据库上下文</param> 5 protected override void Seed(MasterEntityContext context) 6 { 7 //判断admin是否存在,若存在,不新增 8 var dbSet = context.Set<SysUser>(); 9 if (!dbSet.Any(a => a.UserName == "admin")) 10 { 11 var entity = new SysUser { ID = Guid.NewGuid().ToString(), UserName = "admin", Password = "123456", CreateDate = DateTime.Now, CreateUser = "admin" }; 12 //将用户对象附加给数据库上下文(这仅仅是内存级别的操作) 13 dbSet.Add(entity); 14 //数据库上下文保存更改(提交变更到数据库执行) 15 context.SaveChanges(); 16 } 17 }
至此,db.实体库这种调用方式以及全部移除,可以把数据库上下文中的DbSet属性定义的代码移除。
现在来处理注册实体配置的代码:
1 /// <summary> 2 /// 模型配置重写 3 /// </summary> 4 /// <param name="modelBuilder">数据实体生成器</param> 5 protected override void OnModelCreating(System.Data.Entity.DbModelBuilder modelBuilder) 6 { 7 // 禁用一对多级联删除 8 modelBuilder.Conventions.Remove<OneToManyCascadeDeleteConvention>(); 9 // 禁用多对多级联删除 10 modelBuilder.Conventions.Remove<ManyToManyCascadeDeleteConvention>(); 11 // 禁用表名自动复数规则 12 modelBuilder.Conventions.Remove<PluralizingTableNameConvention>(); 13 14 new SysUserConfiguration().RegistTo(modelBuilder.Configurations); 15 new SysRoleConfiguration().RegistTo(modelBuilder.Configurations); 16 }
其中最后2句代码,就是分别对用户配置类、角色配置类的注册。
首先可以确定的是,这里的代码不能省。那既然实体配置类会通过T4直接创建,能否让代码自动去注册实体配置类呢?
答案是可以的。做法也也不止一种。
第一种做法是利用T4自动创建EF数据库上下文,把“实体名称集合”传入模板,自动加上“对每个实体配置类进行注册”的代码。
第二种做法是给实体配置类增加一个“标记”,通过这个标记反射出所有的实体配置类,然后存储在静态变量中,让EF对该静态变量进行循环注册。
很明显,第二种做法优于第一种做法,只需反射一次,还兼顾了代码的简洁优雅。
定义实体配置器接口,名称IEntityConfiguration,并把“各实体配置类中都必须包含的方法”预定义在接口中:
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.ModelConfiguration.Configuration; 7 8 namespace S.Framework.DataCore.EntityFramework 9 { 10 /// <summary> 11 /// 实体配置器接口 12 /// </summary> 13 public interface IEntityConfiguration 14 { 15 /// <summary> 16 /// 将实体配置对象注册到实体生成器配置集合 17 /// </summary> 18 /// <param name="configurations">实体生成器配置集合</param> 19 void RegistTo(ConfigurationRegistrar configurations); 20 } 21 } 22
实现该接口:
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.ModelConfiguration; 7 using System.Data.Entity.ModelConfiguration.Configuration; 8 9 namespace S.Framework.DataCore.EntityFramework 10 { 11 /// <summary> 12 /// 实体配置基类 13 /// </summary> 14 /// <typeparam name="TEntity">实体类型</typeparam> 15 public abstract class EntityConfiguration<TEntity> : EntityTypeConfiguration<TEntity>, IEntityConfiguration where TEntity : class 16 { 17 public EntityConfiguration() 18 { 19 20 } 21 22 public void RegistTo(ConfigurationRegistrar configurations) 23 { 24 configurations.Add(this); 25 } 26 } 27 } 28
接口和实现的文件目录结构如下图:
定义好了接口,也进行了实现。现在去修改T4配置模板,让生成的配置类继承上述的实现类即可。注册方法RegistTo可以去掉了,对EntityTypeConfiguration的继承也可以去掉了。
OK,现在实体配置类都有标记了——都继承于IEntityConfiguration接口,还需要通过一个方法去反射出“继承指定接口的类”,并对这些类初始化后存储在静态变量中,我们可以叫它为“实体配置工厂类”,如下图:
该工厂类通过单例的方式来反射并存储结果。
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.ModelConfiguration.Configuration; 7 8 using S.Utilities; 9 10 namespace S.Framework.DataCore.EntityFramework.ConfigurationFactories 11 { 12 /// <summary> 13 /// 实体配置工厂类 14 /// </summary> 15 public static class MasterConfigurationFactory 16 { 17 /// <summary> 18 /// 同步对象 19 /// </summary> 20 private static readonly object sync = new object(); 21 22 /// <summary> 23 /// 唯一实例 24 /// </summary> 25 private static IEnumerable<IEntityConfiguration> singleton; 26 27 /// <summary> 28 /// 实体配置 29 /// </summary> 30 private static IEnumerable<IEntityConfiguration> Configurations 31 { 32 get 33 { 34 if (singleton == null) 35 { 36 lock (sync) 37 { 38 if (singleton == null) 39 { 40 var types = typeof(IEntityConfiguration).GetSubClass().Where(w => !w.IsAbstract && w.Namespace.EndsWith("Master")); 41 42 singleton = types.Select(m => Activator.CreateInstance(m) as IEntityConfiguration); 43 } 44 } 45 } 46 47 return singleton; 48 } 49 } 50 51 /// <summary> 52 /// 初始化实体模型生成器 53 /// </summary> 54 /// <param name="configurations">实体模型生成器</param> 55 public static void ConfigurationsInit(ConfigurationRegistrar configurations) 56 { 57 foreach (var configuration in Configurations) 58 { 59 configuration.RegistTo(configurations); 60 } 61 } 62 } 63 } 64
注:这里开始将会创建工具类库,并逐步扩充,但不会具体展开解释实现。
通过该配置器工厂中定义的ConfigurationsInit方法,就可以取缔EF数据库上下文中的注册代码了。
修改OnModelCreating如下:
1 /// <summary> 2 /// 模型配置重写 3 /// </summary> 4 /// <param name="modelBuilder">数据实体生成器</param> 5 protected override void OnModelCreating(System.Data.Entity.DbModelBuilder modelBuilder) 6 { 7 // 禁用一对多级联删除 8 modelBuilder.Conventions.Remove<OneToManyCascadeDeleteConvention>(); 9 // 禁用多对多级联删除 10 modelBuilder.Conventions.Remove<ManyToManyCascadeDeleteConvention>(); 11 // 禁用表名自动复数规则 12 modelBuilder.Conventions.Remove<PluralizingTableNameConvention>(); 13 14 MasterConfigurationFactory.ConfigurationsInit(modelBuilder.Configurations); 15 }
别忘了using命名空间。
此时,在数据核心层(S.Framework.DataCore)中再也没有任何实体的直接使用,并且本篇日志开头说的操作步骤中省去了最后2个步骤。
现在当你新增一个实体,只需要运行T4和配置实体字段就可以。
重新编译,再次登录一下,可以跳转至首页就说明没问题。
下一章节,将中途休息,整理和完善前面的内容。
截止本章节,项目源码下载:点击下载(存在百度云盘中)