说要分享,我了个*,写了一半放草稿箱了两个星期都快发霉了,趁着周末写完发出来吧。数据库
文章分为五部分:设计模式
文章的主要部分讲述的是如何利用Entity Framework同时知足数据存储和面向对象应用的最优化,因此整体上能够当作是一大波:数据库这么设计,而后面向对象那边这么用,可让数据(更符合范式/效率更高/更方便管理),而后让面向对象利用数据(更方便/更高效/更安全)。缓存
与许多观点不一样,我认为ORM不只不必定会由于“阻抗失配”致使数据库性能降低、潜能得不到发挥,反而以为ORM能够挖掘出数据库更大的潜能,经过更合理的使用在不少地方提升其使用性能,将数据解放到业务中去。安全
如今有点晚了,我公司在山上,得赶着下山,没来得及审稿,因此有错误欢迎指正。服务器
我的以为有点赶,并且有点长,因此排列文本控制得不是很好,有待再继续补充或者修改。架构
======================分割线,专治强迫症,下面是半个月前的内容-------------------app
上次才说要分享去年的项目,此次一会儿被新的财务系统耗了三个多月,因此就干脆先分享下新系统中的内容。框架
总要有一个点开始展开进行分享,因此就从EF来进行展开,反正EF是贯穿头尾的。若是顺利的话或许还能够有《EF 和 业务逻辑》、《EF 和 Web》、《EF 和 小苹果》......数据库设计
毕竟这篇文章不叫作《Pro EF》之类的讲原理,其目的是讲述在项目中对EF的应用和理解,因此涵盖不全,多多包涵。ide
ORM好比EF的使用有几重境界,不少来讲跟框架自己的能力有关,最好的状况下也不过能停留在挖掘下ORM的功能而已。不少我见到的ORM使用不过是仅仅为了替代SQL,CRUD的思想并无什么改变,全部不少项目还有Repository一层。
数据库是CRUD的,面向对象是“方法与事件”的,何况它们自己就有“阻抗失配”,就不是一个种族的。这里我并不想说明到底哪一个好或者应该如何,我只是想在项目收尾阶段分享一下我在“假如更面向对象”的一次实践。
此次项目是接手去年不成功的财务项目,最终决定由我重作。反正基本上一我的从头至尾,那我就“放肆”下大胆地全OO式地进行。数据做为贯穿项目总体的一方面,已EF为基础,天然占了很重一部分,因此从EF进行扩展也是不错的选择。
第一部分Basic会简单地带过一些我对EF相关基础的“认识”,认识EF的跳过就行了;Class部分特意交代一些面向对象的用法;Business简单说明下在业务中的应用。
项目和示例中都采用Model First,基于EF五、VS 20十二、SQL Server 2012。示例会在实际项目和Demo中穿插引用。
最后不得不提到的是,Entity Framework只有与LinQ相伴才能如此愉快地玩耍,因此中间穿插的LinQ就很少提了。
Entity Framework Model First在开发过程当中适合敏捷的基础是:图形化的设计器、数据库与模型的快速同步。
同步方面,因为个人打算是“更面向对象”,因此同步仅使用用模型生成数据库的方式。首先须要建立项目、在项目中添加ADO.NET Entity Data Model、在数据库服务器中建立相应的数据库。为了方便区分,我使用了相同的命名:
在建立好Model后,在图形设计界面中,右键而后选择将模型同步到数据库中,便可建立相应的SQL;运行该SQL,便可在相应数据库中建立对应的表与关系:
在项目代码层面,在生成Entity Model的同时,会建立一个继承与DbContext的类。该类抽象地说,对应的是指定的数据库,全部对于相关的数据库的访问都“通过”或者“经过”该类。在后来的EF版本中,这个DbContext的派生类基本上都命名为“Entity Model名称+Container”的形式,我习惯称这个类的实例为entities,由于从前的命名方式是“Entity Model名称+Entities”:
顺便简单地说明下CRUD的简单实现方式:
public MyFirstEntity Create() { //建立新的DbContext var entities = new EntityFrameworkDemoContainer(); //建立新的实体 var newMyFirstEntity = new MyFirstEntity(); //将新对象插入到序列中 entities.MyFirstEntities.Add(newMyFirstEntity); //执行数据操做 entities.SaveChanges(); //返回最新建立的实体 return newMyFirstEntity; } public MyFirstEntity Retrieve(int id) { //建立新的DbContext var entities = new EntityFrameworkDemoContainer(); //查找到实体 var myFirstEntity = entities.MyFirstEntities.First(entity => entity.Id == id); //返回查找到的实体 return myFirstEntity; } public void Update(int id) { //建立新的DbContext var entities = new EntityFrameworkDemoContainer(); //查找到实体 var myFirstEntity = entities.MyFirstEntities.First(entity => entity.Id == id); //修改实体 /*此处略去修改实体代码*/ //保存修改 entities.SaveChanges(); } public void Delete(int id) { //建立新的DbContext var entities = new EntityFrameworkDemoContainer(); //查找到实体 var myFirstEntity = entities.MyFirstEntities.First(entity => entity.Id == id); //删除实体 entities.MyFirstEntities.Remove(myFirstEntity); //保存修改 entities.SaveChanges(); }
在设计器中建立相应的Entity,就会在项目中建立相应的Class,同步到数据库后,就会建立相应的Table。Table的名称会是Entity的复数形式,大部分状况下语法是没有错的。
对应地,在上面的CRUD示例中已经说明了如何访问这张表的数据了。
在设计器中,能够为指定的Entity添加属性,添加属性后能够经过Properties进行设计。同步到数据库后,对应的属性会生成对应的Table Column。固然,在项目中,也会在原有的类上添加相应的属性。相关的类型的状态会自动匹配。能够看到用相同颜色标记的对应的对象在不一样方面的体现,好比类型、属性名称、可空性(Nullable)。
在这里,我建立了第二个实体,而后添加了与第一个实体的关联。在添加关联中,我选择了一对多,而且在设计时,使用了默认生成的“引用名”,先看看结果再讲解:
能够看到,在数据库层面,也生成了相应的一对多关系。其实现方法是经过“多”的那张表添加的一个指向“一”表主键的外键而实现的,基本还很好理解。
回到对象层面,能够看到“一”的类MyFirstEntity中多出了一个MySecondEntites的集合属性,要注意命名仍是自动复数的。对应的MySecondEntity里也有一个单一的MyFirstEntity属性。
假设myFirstEntity为一个MyFirstEntity的实例,mySecondEntity为一个MySecondEntity的实例。那么,在C#中,访问myFirstEntity中关联的全部MySecondEntity的方式即为:
myFirstEntity.MySecondEntities
同理,在mySecondEntity中访问相关联的myFirstEntity为:
mySecondEntity.MyFirstEntity
要注意的是,在对象层面的关系中,还有“0..1”,即1个或者0个,在数据库实现中,则是一个可空的外键。
注意我在添加关系的时候,并无勾选“添加外键属性到...”,整个项目中都没有。在以往的经验里,外键容易让开发人员“像操做关系数据库同样操做数据”,致使代码陷入一种很奇怪的情形。
在这里,不得不说到面向对象和关系数据库的“关系”问题。首先插入一个数学式子:
若是用天然语言举例来讲,那就是,若是每一个A有(关联)一个B,每一个B有(关联)一个C,那么每一个A有(关联)一个C。
在说到具体事例前,我再探讨一下数据库范式,细就不讲,数据库范式的做用就是为了使得数据冗余最少,这么讲应该没什么歧义。
那么个人结论就来了:使用EF(或者相似ORM)能使得数据库设计更容易实现更高范式。
举个例子,若是每一个A对应一个B,每一个B对应一个C,...,每一个Y对应一个Z;那么,每一个A对应一个Z。
若是我拥有一个A的实例a,我须要获取其对应的实例z,那么应该怎么实现?
若是是用SQL,那么问题就变成:“若是我有一个A的主键ID,如何获取相关的Z的Row?”
多是我的并不熟悉SQL,因此在我有限的SQL知识看来,我须要些一段很长很长很长很长很长很长很长很长的SQL才能查到我要的数据。因此通常出现这种状况,我会“*他*的范式!”而后直接拉一条A与Z的关联,虽然不只没遵循范式,并且连“环”都产生了。
若是在EF,这么简单地写就能够访问到了:
a.B.C.D.E.F.G.H.I.J.K.L.M.N.O.P.Q.R.S.T.U.V.W.X.Y.Z;
固然,实际上仍是执行了一段可怕的SQL,可是由于用起来简单了,因此更加不须要随意打破范式了。
若是我常常须要从A访问Z,须要:
a.Z;
那么就应该参考后面的“扩展字段”部分了。
复杂一点,若是R(A, B) = 1, R(B, C) = n,那么对应R(A, C)为n没错;若是R(A, B), R(B, C) = n,那么R(A, C) = n ^2,在C#中访问A的C集合固然用LinQ的SelectMany了;若是从A到Z都那么成倍又须要访问,又不想每次写那么多SelectMany嵌套呢?那就只好参考后面“扩展字段”部分了。
在关系数据库中,“关系”仅仅包含着1对n,n对n为两个1对n组合;而在面向对象里,因为对象更多地接近使用天然语言的方式描述“关系”,其“关系”就变得复杂而很差理解了。更多的内容,还须要讲述完下面“继承与关系”才能比较好展开。
另外能够说明下对关系进行复制,也就是添加关联,能够简单地进行对象复制,而不须要触动到外键:
myFirstEntity.MySecondEntites.Add(newMySecondEntity);
这里示范的是一对多的关系,而一对一的状况下只须要直接赋值便可。
虽然做为ORM,必然有一半关于关系数据库,另外一半关于面向对象,而我确把关于关系数据库的部分放到了“基础”。由于整体来讲,我以为ORM的发展是沿着“SQL=>Data Framework(如ADO.NET)=>OO”的趋势下去的,因此把OO部分放到了“更高级别”的地方。
另外,在这种划分状况下,能够大体地理解为我把“存储在数据库中的”放到了基础,把“另外表如今面向对象中的”放到了后面。
既然是面向对象,那固然要有最基础的关系——继承。
在EF中,能够设计两个类为基类子类关系,甚至一个基类多个子类。
一样在设计器中添加继承便可,能够建立完父类子类后再“添加继承”,或者直接在建立子类的时候选择父类:
在设计时,咱们“建立了一个基类和继承与该基类的子类”,而在数据库中则是“建立了一个表以及一张关系与其1对1/0的从表”。在数据库层面,这两张表共用ID,固然了,由于他们是一一对应的关系,同值的主键和直观;在类层面,能够看到只有Base才有ID,Sub在定义上没有,实际上“继承了”Base的ID,也拥有一个ID。
要注意的是,在DbContext处,并无产生Sub的DbSet序列属性,须要经过Base来才能访问获得Sub,使用OfType<TSub>():
var subs = entities.Bases.OfType<Sub>();
插入Sub实例/行的时候,也须要经过Bases:
entites.Bases.Add(new Sub()); entities.SaveChanges();
在表里面都不过是数据字段,而在面向对象(基本上指代C#一类传统面向对象语言)中,是有“访问性”一说。
在上面的基础上,我能够把基类设置为抽象类,把字段设置访问性(public/protected/privite/internal),右键相关的实体/字段,选择属性(Properties):
而在数据库部分,则只是比上面多添加了一个SubProperty的Column而已。用相关访问行的方法,能够很好地在应用层保护数据的逻辑安全性。
抽象类保证了没法在绕过子类(不插入表Bases_Subs数据)的状况下建立基类对象(插入Bases数据),保证了数据上的匹配度;而字段访问行则控制了的读写。
不要被小标题迷惑了,跟“C#扩展方法”没有半毛钱关系,也不是一样层面的事物。这里指的是,在不增长数据库字段的状况下扩充类层面的字段。
实现扩展字段的基础,是全部的模板自定义建立类都是partial的。由于每次更改模板,全部在该Data Model文件下的类,包括DbContext都会从T4模板从新生成一次,因此你没法在原有的类基础上进行任何修改,那么默认类为partial则尤其重要。
只要建立一个类,定义为上面Base的partial类,并添加一个ExtendProperty字段,生成表后与以前并无什么区别,自定义扩展的字段并不会生成到表上:
而后,在空数据库的场景下运行了下面这段代码,能够看到当时Watch的结果(我已经把SubProperty的访问性改回了可写):
而后在运行一次下面的比对代码:
能够看到两次结果大相径庭,第二次的ExtendProperty字段为空。由于第一次获取的是一开始插入Base列表的对象,缓存了起来,而第二次是从新重数据库中读取的对象,数据库中并无该字段,而一开始的缓存中有,因此产生了这项差别。
同时咱们能够参考数据库中的数据:
同时也能够注意到,Bases_Sub表的主键是可读写的,而Bases表的主键是自增的。也就是说Bases_Sub表的主键是同步自Bases表对应的条目。
经过扩展字段,咱们能够很好地让不少重用性高的访问方式被“优雅地”封装起来,好比下面跟/子节点/孙节点的例子:
在这里,我建议列表中尽可能返回LinQ表达式值,而尽可能不返回任何包含实例化行为的序列,以进行性能保护。
不该随意使用扩展字段做为LinQ查询条件,由于在数据库中不存在的字段势必会形成全集无索引的遍历。
至于计算出的扩展的字段,我建议尽可能使用缓存,而不每次都进行计算。
有了上面的的基础和扩展字段,就能够进行一些“多态”一点的行为。
继续上面的例子,在基类上添加一个抽象方法,而后“迫使”子类必须实现该方法:
在这里要注意的是Base自己就是虚类,但在partial里不须要重复声明,写上也无妨。
跟上面的扩展出来的字段同样,在表中都不会产生字段。
有了上面的建模,以后就是如何使用进行业务操做的问题了。
在一次EF的数据操做中,整体上是包含下面四部分:
最后一步在写操做的时候才会发生/使用。
其中要注意的是,全部操做都是围绕同一个DbContext实例进行的。
首先看看下面代码,firstBase和firstBase2的值如同上面所设定,显而易见:
再往下运行才是我想说的重点:
能够看到,即便获取对象在保存对象以后,存在明显的先写后读的关系,不一样DbContext(文中的entities和entites2)对象取出的数据所在历史阶段也不一致。
也就是说,一旦一个DbContext获取过某个数据以后,就会产生数据缓存,而该缓存的做用域在于该DbContext上。
一样的道理能够扩充到:对同一个DbContext获取到的对象进行修改,只能经过该DbContext进行保存修改。
特别是还要扩充到:只有来自同一个DbContext的对象才能够进行关联而且保存,不然在保存的时候会发生异常。
正因如此,才会产生下面的“协做与事务”的问题,也是这一节的三个小节衍生源。
什么是面向对象的工做场景?我看到印象最深入的解释竟然是来自iOS开发的官方入门教程,大体的内容就是说,“你能够认为是一群存在的对象互相协做并完成工做。”翻不出来了,不过大体如此。
在这里我并不想彻底“颠覆”纯仓储模式,只是CRUD地工做让我以为很别扭,当方法足够多的时候难以对业务进行重用等等。而在使用EF的时候我也再也不写一个包含Repository的层,由于我以为不少余,另外一个就是我但愿尽可能地减小层次,以简化系统复杂度。
另外摘录DbContext的注释的第一段话,或许比较有参考价值:
上面说到,一个DbContext实例“已是”一个工做单元和仓储模式的混合。这也很容易理解,由于一个DbContext实例已经包含了CRUD全功能。不过这个实例如同业务的需求同样,它是完整的,也是“残缺”的。若是只CRUD那么每次业务就是:选取数据=》更新数据=》持久化。而数据只是系统运行时的业务状态值,而不是系统自己。系统的目的是为了实现业务而存在的,而不是持有业务的数据。
回到这里,即时DbContext包含了CRUD,它也没有很好地表达出业务。举例说,用户提交一张申请单,若是咱们将其解释为“用户申请XXX”,那么咱们但愿见到代码中表现出来的不是:
entities.Applications.Add(new Application { Title = xxx, Balance = xxx }); entities.SaveChanges();
而是:
applicationService.Apply(title, balance);
下者不过是上者的一个封装,可是却又不同的意义。咱们使用OO是为了让代码更贴近现实,而让创造出的系统更加符合现实业务需求。也就是只使用DbContext只能描述对数据的操做,却没法描述业务,因此咱们须要一个业务层。
描述业务能够有不少种甚至无数种建模,由于现实中人看问题有不少种角度,好比同为申请单业务,也能够这么理解:
user.ApplyBalance(title, balance);
“让用户去申请”,看似也没错。甚至在建模中能够用不少夸张的、绚丽的设计模式,但那倒是最糟糕的设计模式,由于模式太多了、没法维护、浪费代码等等。
我须要一个设计的方式,让整体保持一致,同时易于理解。借鉴DDD,我把在业务中存在的对象划分红了两个角色:实体与劳工。
实体指代的是数据对象,而劳工则是管理、操做、加工数据对象的工做者。“实体”大多数状况下就是一个定义好的EF对象,毕竟我使用EF做为主要的数据操做方式。
这与传统的三层架构很类似,或许是异曲同工。我在劳工使用的后缀为“Manager”,或者少数状况为了细化,一样会使用工做者的命名方式,好比:Helper、Provider、Worker......重要的是,我把它们抽象成了一个显示存在的运行机器或者团队,有着像工程运行同样的从属关系、合做关系。
若是要有个贴切的简述,那就是:劳工是有主动性的有生命的,实体则是劳工们劳做中的操做对象和产出,同时也不排除一个领域的劳工和实体同时存在,好比用户(User)和管理用户的劳工(UserManager)。
我相信每一个劳工的职责都应该是单一的、封闭的,因此将全部劳工都设计成职责单一封闭。而实现的依据即是一开始的需求文档:
左边是文档中的结构,右边是代码中的结构,基本上能与文档一一对应。实现的类的描述也是尽可能语义化,这样作的目的是为了贴合需求,并且减小代码与需求间的阻隔,让代码更易维护。这里咱们仍是讲EF的文章,所就很少作扩展了。
粗俗地说,能够理解为,『汇率Manager』就只管理汇率,而『申请单Manager』就只管理申请单。
劳工们的关系也简单地划分为两种:
固然,其中的缺点也就是没法进行循环依赖。
这里有一个灰色地带要划清界限,也就是获取关联数据的问题。
假如你拥有A类的实体a和老公AManager,A与B类有关系,那么应该直接经过a获取相关的b,即 b = a.B; ,而再也不经过AManager获取。由于设计中认为,全部与A相关的实体,即A锁拥有的“关系”,都应该是A的特性/属性,是A的一部分。
后面会讲到Entity的设计模式以达成相关任务。
上面或许已经发现一个矛盾了:EF全部原子化的操做都依赖于单一的DbContext实例,若是我把指责拆分并封闭起来,那么他们就必须各自使用本身的DbContext对象,而没法共同处理事务。
如下解决方案能够简单否决:
而我能想到的最优雅的解决方案就是:在须要共同执行事务的时候注入DbContext并,而且用事务进行包裹。
首先,在每一个劳工都依赖于一个DbContext的状况下,我写了一个公共接口与基类分别为:
1 /// <summary> 2 /// 依赖于Entity Framework作数据存储的方法的类的接口 3 /// 包含同步Entity Framework DbContext的方法,已实事务处理 4 /// </summary> 5 public interface IEfDbContextDependency 6 { 7 /// <summary> 8 /// 设置当前对象的Entity Framework DbContext 9 /// </summary> 10 /// <param name="dbContext">传入的dbContext</param> 11 void SetDbContext(Finance2Container dbContext); 12 13 /// <summary> 14 /// 设置当前对象的Entity Framework DbContext 15 /// </summary> 16 /// <param name="dbContext">传入的DbContext</param> 17 /// <param name="lastContext">当前对象原来使用的DbContext</param> 18 void SetDbContext(Finance2Container dbContext, out Finance2Container lastContext); 19 20 /// <summary> 21 /// 临时注入DbContext,并执行Callback 22 /// 在执行过程当中会用lock锁住本对象 23 /// 在执行完成后会被还原 24 /// </summary> 25 /// <param name="dbContext">须要注入的DbContext</param> 26 /// <param name="callback">回调</param> 27 void InjectDbContextTemporarily(Finance2Container dbContext, Action callback); 28 29 /// <summary> 30 /// 临时注入DbContext,并执行Callback 31 /// 在执行过程当中会用lock锁住本对象 32 /// 在执行完成后会被还原 33 /// </summary> 34 /// <typeparam name="TOut">回调后返回的类型</typeparam> 35 /// <param name="dbContext">须要注入的DbContext</param> 36 /// <param name="callback">回调</param> 37 /// <returns>回调后返回值</returns> 38 TOut InjectDbContextTemporarily<TOut>(Finance2Container dbContext, Func<TOut> callback); 39 }
1 /// <summary> 2 /// 依赖于Entity Framework作数据存储的方法的类的基类 3 /// 包含同步Entity Framework DbContext的方法,已实事务处理 4 /// </summary> 5 public abstract class EfDbContextDependencyBase : IEfDbContextDependency 6 { 7 #region dependencies 8 9 /// <summary> 10 /// DbContext对象,以Repoisitory模式设计,因此使用仓库名entities 11 /// 任何对entities的赋值操做都会对iEfOperatorsToSync进行同步 12 /// </summary> 13 /// <value>The entities.</value> 14 protected Finance2Container entities 15 { 16 get 17 { 18 return this._entities; 19 } 20 set 21 { 22 this._entities = value; 23 24 //同步DbContext 25 this.syncIEfDbContextDependencies(); 26 } 27 } Finance2Container _entities; 28 29 #endregion 30 31 32 #region constructors 33 34 /// <summary> 35 /// 初始化<see cref="EfDbContextDependencyBase" />类 36 /// </summary> 37 /// <param name="_entities">必须传入一个DbContext对象</param> 38 /// <param name="efDbContextDependencies">须要自动同步DbContext的业务对象</param> 39 public EfDbContextDependencyBase(Finance2Container _entities, params IEfDbContextDependency[] efDbContextDependencies) 40 { 41 this.entities = _entities; 42 43 //设置业务对象DbContext自动同步 44 this.SetSyncIEfDbContextDependencies(efDbContextDependencies); 45 } 46 47 #endregion 48 49 50 #region private methods 51 52 /// <summary> 53 /// 须要同步DbContext的IEfDbContextDependency序列 54 /// </summary> 55 private IEfDbContextDependency[] iEfOperatorsToSync; 56 57 /// <summary> 58 /// 同步IEfOpertors中的DbContext 59 /// </summary> 60 private void syncIEfDbContextDependencies() 61 { 62 if (iEfOperatorsToSync != null) 63 { 64 foreach (var @operator in iEfOperatorsToSync) 65 { 66 @operator.SetDbContext(this.entities); 67 } 68 } 69 } 70 71 /// <summary> 72 /// 设置须要同步DbContext的IEfDbContextDependency对象,设置以后被设置的对象会在本对象被调用时 73 /// 设置以后,当本对象的entities被做任何set操做都会致使operators内容的同步 74 /// </summary> 75 /// <param name="operators">The operators.</param> 76 protected void SetSyncIEfDbContextDependencies(params IEfDbContextDependency[] operators) 77 { 78 this.iEfOperatorsToSync = operators; 79 80 //经过从新复制entities进行同步 81 this.entities = this.entities; 82 } 83 84 /// <summary> 85 /// 临时注入DbContext,并执行Callback 86 /// 在执行过程当中会用lock锁住全部注入对象 87 /// 在执行完成后会被还原 88 /// </summary> 89 /// <param name="dbContext">须要注入的DbContext</param> 90 /// <param name="callback">回调</param> 91 /// <param name="efDbContextDependencies">须要被注入的对象列表</param> 92 protected void InjectAllDbContextsTemporarily(Finance2Container dbContext, Action callback, params IEfDbContextDependency[] efDbContextDependencies) 93 { 94 if (efDbContextDependencies == null || efDbContextDependencies.Count() == 0) 95 { 96 callback(); 97 return; 98 } 99 //递归注入 100 efDbContextDependencies 101 .First() 102 .InjectDbContextTemporarily(dbContext, 103 () => 104 { 105 this 106 .InjectAllDbContextsTemporarily(dbContext, callback, efDbContextDependencies.Skip(1).ToArray()); 107 }); 108 } 109 110 /// <summary> 111 /// 临时注入本对象的DbContext,并执行Callback 112 /// 在执行过程当中会用lock锁住全部注入对象 113 /// 在执行完成后会被还原 114 /// </summary> 115 /// <param name="callback">回调</param> 116 /// <param name="efDbContextDependencies">须要被注入的对象列表</param> 117 protected void InjectAllDbContextsTemporarily(Action callback, params IEfDbContextDependency[] efDbContextDependencies) 118 { 119 this 120 .InjectAllDbContextsTemporarily(this.entities, callback, efDbContextDependencies); 121 } 122 123 /// <summary> 124 /// 临时注入DbContext,并执行Callback 125 /// 在执行过程当中会用lock锁住全部注入对象 126 /// 在执行完成后会被还原 127 /// </summary> 128 /// <typeparam name="TOut">回调后返回的类型</typeparam> 129 /// <param name="dbContext">须要注入的DbContext</param> 130 /// <param name="callback">回调</param> 131 /// <param name="efDbContextDependencies">须要被注入的对象列表</param> 132 /// <returns>回调后返回值</returns> 133 protected TOut InjectAllDbContextsTemporarily<TOut>(Finance2Container dbContext, Func<TOut> callback, params IEfDbContextDependency[] efDbContextDependencies) 134 { 135 if (efDbContextDependencies == null || efDbContextDependencies.Length == 0) 136 { 137 return callback(); 138 } 139 //递归注入 140 return efDbContextDependencies[0] 141 .InjectDbContextTemporarily(dbContext, 142 () => 143 { 144 return this 145 .InjectAllDbContextsTemporarily(dbContext, callback, efDbContextDependencies.Skip(1).ToArray()); 146 }); 147 } 148 149 /// <summary> 150 /// 临时注入本对象的DbContext,并执行Callback 151 /// 在执行过程当中会用lock锁住全部注入对象 152 /// 在执行完成后会被还原 153 /// </summary> 154 /// <typeparam name="TOut">回调后返回的类型</typeparam> 155 /// <param name="callback">回调</param> 156 /// <param name="efDbContextDependencies">须要被注入的对象列表</param> 157 /// <returns>回调后返回值</returns> 158 protected TOut InjectAllDbContextsTemporarily<TOut>(Func<TOut> callback, params IEfDbContextDependency[] efDbContextDependencies) 159 { 160 return this 161 .InjectAllDbContextsTemporarily(this.entities, callback, efDbContextDependencies); 162 } 163 164 #endregion 165 166 167 #region IEfDbContextDependency members 168 169 /// <summary> 170 /// 设置当前对象的Entity Framework DbContext 171 /// </summary> 172 /// <param name="dbContext">传入的dbContext</param> 173 public virtual void SetDbContext(Finance2Container dbContext) 174 { 175 this.entities = dbContext; 176 } 177 178 /// <summary> 179 /// 设置当前对象的Entity Framework DbContext 180 /// </summary> 181 /// <param name="dbContext">传入的DbContext</param> 182 /// <param name="lastContext">当前对象原来使用的DbContext</param> 183 public virtual void SetDbContext(Finance2Container dbContext, out Finance2Container lastContext) 184 { 185 lastContext = this.entities; 186 187 this.SetDbContext(dbContext); 188 } 189 190 /// <summary> 191 /// 临时注入DbContext,并执行Callback 192 /// 在执行过程当中会用lock锁住本对象 193 /// 在执行完成后会被还原 194 /// </summary> 195 /// <param name="dbContext">须要注入的DbContext</param> 196 /// <param name="callback">回调</param> 197 public virtual void InjectDbContextTemporarily(Finance2Container dbContext, Action callback) 198 { 199 //在强行替换DbContext时须要锁住对象以避免出现数据乱流问题 200 lock (this) 201 { 202 //替换并暂存原有DbContext 203 Finance2Container lastFinanceDbContext; 204 this.SetDbContext(dbContext, out lastFinanceDbContext); 205 206 //回调 207 callback(); 208 209 //还原原有DbContext 210 this.SetDbContext(lastFinanceDbContext); 211 } 212 } 213 214 /// <summary> 215 /// 临时注入DbContext,并执行Callback 216 /// 在执行过程当中会用lock锁住本对象 217 /// 在执行完成后会被还原 218 /// </summary> 219 /// <typeparam name="TOut">回调后返回的类型</typeparam> 220 /// <param name="dbContext">须要注入的DbContext</param> 221 /// <param name="callback">回调</param> 222 /// <returns>回调后返回值</returns> 223 public virtual TOut InjectDbContextTemporarily<TOut>(Finance2Container dbContext, Func<TOut> callback) 224 { 225 //在强行替换DbContext时须要锁住对象以避免出现数据乱流问题 226 lock (this) 227 { 228 //替换并暂存原有DbContext 229 Finance2Container lastFinanceDbContext; 230 this.SetDbContext(dbContext, out lastFinanceDbContext); 231 232 //获取执行结果 233 var result = callback(); 234 235 //还原原有DbContext 236 this.SetDbContext(lastFinanceDbContext); 237 238 //返回执行结果 239 return result; 240 } 241 } 242 243 #endregion 244 }
里面重要的是“Inject”一类的代码,做用也就是将DbContext实例注入到劳工中,使得在相关事务中劳工们使用的是同一个DbContext,为了方便,也附带了泛型和集体注入的实现方式。暂时来讲我的以为这是个很别扭的办法,以前一直在寻求一种自动的依赖注入方式,但愿至少能将DbContext注入到一次HttpRequest中,有实现方法的朋友能够@下我,不胜感激。
另外一件须要提到的就是事务,是这个事务(Transaction),须要使用Transaction才能把一件原子化的事务包装起来,以保证相关业务操做如设想同样,好比申请单若是提交到一半发生异常,那么应该是都不提交。
加上事务后的用法,应该是:
1 voic DoSomeWork() 2 { 3 this.InjectAllDbContextsTemporarily(() => 4 { 5 using (scope = new TransactionScope()) 6 { 7 this.doMyJob(); 8 bManager.DoOtherJobs(); 9 scope.Complete(); 10 } 11 }, bManager); 12 }
也请注意引用System.Transactions。
本例,若是BManager中也有事务会不会有影响呢?不会,由于事务是能够嵌套的,最后执行时会是最外层的TransactionScope.Complete()方法。
这是我以为在使用EF时最有趣的地方,由于你能够在数据模型的基础上根据本身的须要,为其添加跟多的业务特性,而无需影响到数据自己。不少时候,由于这些特性,使得你能够更加倾向于就此解决问题,而不打破范式以求全。整体来讲,如下设计模式的目的就是为了保持数据与其结构的纯洁性的状况下知足业务的遍历性。
若是存在一个属性或引用,它的名称不符合业务描述,那么能够将它重命名。
有两种方式,一种是在设计器里,将其重命名,好比A.User改为A.Friend,以符合业务指望的描述。但有时候,这样是不足以知足需求的,那么就使用扩展方法将其重命名:
public partial A { public User Friend { get { return this.B } } }
参考上面关系部分的说明,若是R(A, C) = 1,存在A.B和B.C,那么咱们能够在不建立的A.C真实数据的情形下,建立A.C做为A.B.C的重命名:
public partial A { public C C { get { return this.B.C; } } }
或许如下命名方式会更清晰,获取用户全部书,假设用户全部书都放在一个书柜上:
public partial User { public IEnumerable<Book> MyBooks { get { return this.Bookcase.Books; } } }
在一些带有业务特性的状况下,能够在重命名的基础上添加相关业务操做:
public partial User { public string FullName { get { return String.Format("{0} {1}", this.FirstName, this.LastName); } } }
不过要注意的是,因为一般使用LinQ操做,因此应尽可能使用缓存,好比延迟加载:
public partial User { public string FullName { get { if (_fullName == null) { _fullName = String.Format("{0} {1}", this.FirstName, this.LastName); } return _fullName; } } string _fullName; }
固然,同时还能够扩充为筛选计算值:
public partial User { public User BestFriend { get { return this.Friends.Max(friend => friend.Wealth + friend.GookLooking + friend.Height); } } }
在这种状况下就不那么建议使用延迟加载或者缓存了,若是Max是LinQ方法,或者换成是Where、Select之类的LinQ方法,那么留有LinQ自有的延迟加载特性比较好。
在计算的基础上,能够将两个同类序列合并:
public partial BankAccount { public IEnumerable<Record> AllRecords { get { return this.InputRecords.Union(this.OutputRecords); } } }
若是对于数据,访问行须要进行限制,已保证业务对数据的读写安全,那么就将其封闭。
假如class Account有一字段Password,而该字段须要进行加密方能存储,那么能够将数据的字段Password的访问行设置为私有的,而后重写其读写方法:
//此部分为EF自动生成部分 public Account { public string Password { private get; private set; } } public partial Account { public EncryptedPassword { get { return this.Password; } } public SetPassword(string originalPassword) { this.Password = EncryptHelper.Encrypt(originalPassword); } }
在一个“基类——派生类群”的关系集合里,若是数据的入口为基类的集合,而处理中须要分类,则须要经过继承约束,即多态实现肯定其具体特性。
固然,你可使用typeOf,或者OfType<T>()来进行归类,但前者代码不太美观,后者性能较低,每次都要遍历。
这里是其中一种示例,首先存在如下关系,并经过设计器建立了一个Enum(枚举)。其中环境须要在.NET Framework 4.5+和EF 5+,不然枚举也能够直接在项目中添加而不经过EF设计器:
而后,现有基类的扩展:
public partial class BaseType { public abstract MyType Type { get; } }
而后分别扩展派生类:
public partial class AType { public override MyType Type { get { return MyType.A; } } }
public partial class AType { public override MyType Type { get { return MyType.A; } } }
那么,在使用中,便可分类处理:
var entities = new EntityFrameworkDemoContainer(); var allTypes = entities.BaseTypes; foreach (var type in allTypes) { switch (type.Type) { case MyType.A: Console.WriteLine("It's an A."); break; case MyType.B: Console.WriteLine("It's an B."); break; } }
在实际应用中,还能够定义更多的方法、特性,好比序列化。
当两个没有类型相关的实体拥有相同特性的时候,在针对某项共性的处理中,能够将它们归类。
这种方式一般用共有的接口来实现,用接口来描述(非约束)其共有的特性。
好比存在以下关系:
若是我须要遍历全部引用了(依赖于)Component的对象,那么我须要将全部引用者同质化,依次建立如下代码:
public interface IComponentDependency { Component Component { get; set; } }
public partial class Car : IComponentDependency { }
public partial class Plane : IComponentDependency { }
public partial class EntityFrameworkDemoContainer { public IEnumerable<IComponentDependency> AllComponentDependencies { get { return ((IEnumerable<IComponentDependency>)this.Cars) .Union(this.Planes); } } }
能够看到,经过建立带有Component的IComponentDependency接口,而后并为Car和Plane添加此接口,变能够将全部Car和Plane归类为ComponentDependencies而不须要经过它们拥有共同的父类实现。而使用方面,则能够:
var entities = new EntityFrameworkDemoContainer(); var allComponentDependencies = entities.AllComponentDependencies; foreach (var componentDependency in allComponentDependencies) { Console.WriteLine("I find a component."); }
当全部业务,都是由工做实体(Unit of Work),以上定义的劳工(Worker)进行操做完成的。因为DbContext操做的原子性,不一样的Worker间须要共同工做就须要跨越此障碍。
劳工们一般有不少类似的工做内容,因此在接口设计的时候能够提供一些公共接口或者实现:
经过ID获取具体实体的接口:
/// <summary> /// 用ID获取Entity的接口 /// </summary> /// <typeparam name="TEntity">Entity的类型</typeparam> public interface IId<TEntity> { /// <summary> /// 获取拥有ID的Entity /// </summary> /// <param name="id">ID</param> /// <returns>Entity</returns> TEntity this[int id] { get; } }
获取全部同类实体:
/// <summary> /// 全部Entity的接口 /// </summary> /// <typeparam name="TEntity">Entity的类型</typeparam> public interface IAll<TEntity> { /// <summary> /// 获取全部Entity /// </summary> /// <value>全部Entity</value> IEnumerable<TEntity> All { get; } }
建立、添加、移除:
/// <summary> /// 建立实体的接口 /// </summary> /// <typeparam name="TEntity">Entity的类型</typeparam> public interface ICreate<TEntity> { /// <summary> /// 建立新Entity /// </summary> /// <returns>新Entity</returns> TEntity Create(); }
/// <summary> /// 添加Entity的接口 /// </summary> /// <typeparam name="TEntity">Entity的类型</typeparam> public interface IAdd<TEntity> { /// <summary> /// 添加一个新项 /// </summary> /// <param name="newItem">须要添加的新项</param> /// <returns>添加后的项</returns> TEntity Add(TEntity newItem); }
/// <summary> /// 移除Entity的接口 /// </summary> /// <typeparam name="TEntity">Entity的类型</typeparam> public interface IRemove<TEntity> { /// <summary> /// 移除Entity的接口 /// </summary> /// <param name="id">Entity的ID</param> void Remove(int id); }
在项目中,我并无提供更新的方法,由于严格地分层来讲,上层没法跨越业务沾染数据层面的DbContext对象,天然不存在更改Entity的行为。同时也为了业务安全和规范约束,全部更改操做都应该经过业务层面进行操做。若是只是传参实体,业务层则没法把握UI等上层到底对Entity进行了什么修改,甚至有可能对Entity相关的其余Entity进行跨越业务的修改。在通常状况下,连IAdd也不该该有。
当一个Worker须要另外一个Worker的协做,则直接依赖并引用对象Worker,即“借助”该Worker。
假设有一更新申请单的方法,须要当前用户方能进行操做,则涉及了UserManager,ApplicationManager共同工做的内容,实现当以下:
public interface IApplicationManager : IEfDbContextDependency , IId<Application> { void Edit(int applicationId, string content); }
public class ApplicationManager : EfDbContextDependencyBase , IApplicationManager { IUserManager userManager; public ApplicationManager(IUserManager userManager , MyContainer entities) : base(entities) { this.userManager = userManager; } public Application this[int id] { get { return this.entities.Applications.FirstOrDefault(application => application.Id == id); } } public void Edit(int applicationId, string content) { //获取相关申请单 var application = this[applicationId]; //获取当前用户 var currentUser = userManager.CurrentUser; //只有当当前用户为申请人才能够修改 if (application.Applicant.Id == currentUser.Id) { //修改申请单 ... this.entities.SaveChanges(); } } }
当本劳工需所处理的实体产品须要依赖于其余劳工提供的产品,那么就须要同化两者的DbContext。
好比须要添加申请单,那么就须要将当前用户做为申请单的申请人,引用上面的代码并省略一部分:
public partial ApplicationManager { public Application Apply(string content) { userManager.InjectDbContextTemporarily(this.entities, () => { //获取当前用户,做为申请人 var currentUser = userManager.CurrentUser; var newApplication = new Application { Application = currentUser, Content = content }; this.entities.Applications.Add(newApplication); this.entities.SaveChanges(); return newApplication; }); } }
当一个业务,须要多个Worker合做完成,而且该工序具备原子性,则将它们封装在一个事务里。
这里,先用项目中的一段代码进行示范,注释和代码都是项目中的:
1 /// <summary> 2 /// 添加新汇率 3 /// </summary> 4 /// <param name="exchangeRateRuleId">汇率规则ID</param> 5 /// <param name="time">添加到的时间点</param> 6 /// <param name="rate">比率</param> 7 /// <param name="inverseRate">反向比率</param> 8 /// <returns>新添加的汇率</returns> 9 /// <exception cref="GM.OA.Finance2.Domain.FinancialBase.ExchangeRate.RateIsZeroException">比率或反向比率为0时抛出此异常</exception> 10 /// <exception cref="GM.OA.Finance2.Domain.FinancialBase.ExchangeRate.ExchangeRateAlreadyExistException">当天的汇率已设置时抛出此异常</exception> 11 public DataAccess.ExchangeRate Add(int exchangeRateRuleId, DateTime time, decimal rate, decimal inverseRate) 12 { 13 //比率不得为0 14 if (rate == 0 || inverseRate == 0) 15 { 16 throw new RateIsZeroException(); 17 } 18 19 //若是同时间点汇率已存在,则抛出异常 20 var existExchangeRate = entities 21 .ExchangeRates 22 .FirstOrDefault(item => 23 item.ExchangeRateRule.Id == exchangeRateRuleId 24 && item.Time == time); 25 if (existExchangeRate != null) 26 { 27 throw new ExchangeRateAlreadyExistException(existExchangeRate); 28 } 29 30 //获取汇率规则 31 var exchangeRateRule = this.ExchangeRateRuleManager[exchangeRateRuleId]; 32 33 //建立一对正反的兑换汇率 34 //正兑换汇率 35 var exchangeRate = new DataAccess.ExchangeRate 36 { 37 Time = time, 38 ExchangeRateRule = exchangeRateRule//关联汇率规则 39 }; 40 //反兑换汇率 41 var inverseExchangeRate = new DataAccess.ExchangeRate 42 { 43 Time = time, 44 ExchangeRateRule = exchangeRateRule.InverseExchangeRateRule//关联反向的汇率规则 45 }; 46 exchangeRate.InverseExchangeRate = inverseExchangeRate;//互相设置为反向汇率 47 //设置汇率 48 exchangeRate.SetRate(rate, inverseRate); 49 50 //因为汇率互为反向汇率,致使数据对象环状,因此须要规范顺序进行事务插入 51 using (var scope = new TransactionScope()) 52 { 53 //持久化数据 54 this.entities.ExchangeRates.Add(exchangeRate); 55 this.entities.SaveChanges(); 56 57 //设置反向汇率 58 inverseExchangeRate.InverseExchangeRate = exchangeRate; 59 this.entities.SaveChanges(); 60 61 scope.Complete(); 62 } 63 64 //返回插入后的新对象 65 return exchangeRate; 66 }
这是插入一个新的汇率,好比¥兑换$的汇率,同时也要建立$兑换¥的汇率,因为有计算偏差和一些买卖差价,因此两个汇率不必定严格按照反比存在,而是手动设定的值,这是业务部分。而在实现部分,能够知道这样的状况下,一个汇率会关联两个汇率规则(¥<=>$、$<=>¥),那么就造成了“环”,插入的时候没法单步执行,因此须要逐步执行(屡次SaveChanges())并经过Scope进行封装。我想,在事务方面没什么疑问。
那么,在上文里,说到过,事务是能够嵌套的,而嵌套以最外层的TransactionScope.Complete()为最终执行点。当你须要多个不一样的Work共同进行操做,而且该操做具备原子性的时候,你能够在注入DbContext后用TransactionScope将其包裹:
1 /// <summary> 2 /// 从指定帐户转入指定金额到另外一个指定帐户 3 /// 兑换四舍五入保留两位小数 4 /// </summary> 5 /// <param name="withdrawalAccountId">转出帐户ID</param> 6 /// <param name="depositAccountId">转入帐户ID</param> 7 /// <param name="transferAmount">转帐金额</param> 8 /// <param name="withdrawalCurrencyCode">币别英文单位,为空则使用默认币别,通常认为是人民币</param> 9 /// <param name="exchangeRateId">汇率ID,若是不为空则使用该汇率进行转帐</param> 10 /// <param name="remarks">备注</param> 11 /// <returns>转帐日志</returns> 12 public TBalanceTransferRecord TransferTo(int withdrawalAccountId, int depositAccountId, 13 decimal transferAmount, 14 string depositCurrencyCode = null, 15 int? exchangeRateId = null, 16 string remarks = null) 17 { 18 //若是币别为空则设置默认币别 19 if (depositCurrencyCode == null) 20 { 21 depositCurrencyCode = Configurations.DefaultCurrencyCode; 22 } 23 24 //处理汇率和转入转出具体值 25 DataAccess.ExchangeRate exchangeRate; 26 decimal withdrawalAmount; 27 decimal depositAmount = transferAmount; 28 29 //根据不一样的汇率状况计算转入转出金额 30 if (exchangeRateId == null)//没有汇率的状况 31 { 32 exchangeRate = null; 33 //没有汇率的状况下转入和转出对等 34 withdrawalAmount = depositAmount; 35 } 36 else 37 { 38 //获取汇率 39 exchangeRate = exchangeRateManager[exchangeRateId.Value]; 40 41 //若是汇率为空,则抛出异常 42 if (exchangeRate == null) 43 { 44 throw new ExchangeRateNullException(exchangeRateId.Value); 45 } 46 47 //若是汇率的外币不是转入币别,则使用反向汇率 48 if (exchangeRate.ExchangeRateRule.ForeignCurrency.Code != depositCurrencyCode) 49 { 50 exchangeRate = exchangeRate.InverseExchangeRate; 51 } 52 53 //若是汇率外币依旧不是转入币别,则没法使用该汇率,抛出异常 54 if (exchangeRate.ExchangeRateRule.ForeignCurrency.Code != depositCurrencyCode) 55 { 56 throw new ExchangeRateNotMatchException(currencyManager[depositCurrencyCode], exchangeRate); 57 } 58 59 //转出金额经过汇率换算,四舍五入保留两位小数 60 withdrawalAmount = Math.Round(depositAmount * exchangeRate.InverseExchangeRate.Rate, 2, MidpointRounding.AwayFromZero); 61 } 62 63 //须要使用如下汇率业务,注入DbContext 64 var record = exchangeRateManager 65 .InjectDbContextTemporarily(this.entities, () => 66 { 67 //将转入转出集中在一个事务中 68 using (var scope = new TransactionScope()) 69 { 70 var withdrawalRecord = this 71 .AdjustBalance(withdrawalAccountId, 72 -withdrawalAmount,//!Attension!: 注意这里为负数,由于为转出 73 exchangeRate == null 74 ? depositCurrencyCode 75 : exchangeRate.ExchangeRateRule.DomesticCurrency.Code); 76 var depositRecord = this 77 .AdjustBalance(depositAccountId, 78 depositAmount, 79 depositCurrencyCode); 80 81 //建立新日志 82 var newTransferRecord = createNewTransferRecord(withdrawalRecord, depositRecord, transferAmount, exchangeRate); 83 84 //若是类型不匹配,则抛出异常 85 if (newTransferRecord.GetType() != typeof(TBalanceTransferRecord)) 86 { 87 throw new BalanceTransferRecordTypeNotMatchException<TBalanceTransferRecord>(); 88 } 89 90 //设置日志内容 91 var transferRecord = (TBalanceTransferRecord)newTransferRecord; 92 transferRecord.WithdrawalBalanceChangeRecord = withdrawalRecord; 93 transferRecord.DepositBalanceChangeRecord = depositRecord; 94 transferRecord.ExchangeRate = exchangeRate; 95 transferRecord.Remarks = remarks; 96 97 //持久化日志 98 this.entities.BalanceTransferRecords.Add(transferRecord); 99 this.entities.SaveChanges(); 100 101 //结束事务 102 scope.Complete(); 103 104 return transferRecord; 105 } 106 }); 107 108 return record; 109 }
这也是项目中的代码,即从一个帐户用某个汇率转帐必定金额到另外一个帐户。
每次修改Entity Model后,直接运行生成的SQL,就能够得到新的数据库了,但同时也清空了原来的数据。
因此我须要一个初始化开发测试数据的方式,便在项目中添加了初始化方法,建立了一个简单(lou)的初始化工具,方便初始化一些数据:
有了初始化工具后,项目的迭代方式大体是:修改Entity Model =》 从新生成数据库 =》 修改/不修改初始化工具 =》 初始化数据。哪怕是一个字段的修改,均可以立刻更新数据库设计,而后继续下一步开发。
固然,这个流程却不适用在生产机上,这是如今遇到的一个难题。若是发布生产后,数据库中有了真实数据,那么更改就要涉及数据迁移的事了,事情就变得不那么简单。