Microsoft NLayerApp案例理论与实践 - 基础结构层(数据访问部分)
上篇文章讲解了NLayerApp案例的基础结构层(Cross-Cutting部分),现在,让我们继续解读NLayerApp的基础结构层(数据访问部分)。NLayerApp的基础结构层(数据访问部分)包含如下内容:Unit Of Work(PoEAA)、仓储的具体实现、NLayerApp的数据模型以及与测试相关的类。下面,我们将对前三个部分进行讨论,与测试相关的内容,我打算最后单独一章进行介绍。
Unit Of Work(PoEAA)
Unit Of Work(UoW)模式在企业应用架构中被广泛使用,它能够将Domain Model中对象状态的变化收集起来,并在适当的时候在同一数据库连接和事务处理上下文中一次性将对象的变更提交到数据中。在没有引入UoW之前,你可以在每次增加、删除对象或者更改对象状态后,直接调用数据库以保存对象的变化,但这样做会导致应用程序对数据库这一外部技术架构的频繁访问,严重影响了系统性能。这就好像我们打开Notepad进行文字编辑一样,我们完全可以每输入一个字符,就按下Ctrl+S保存一次,但这样做非常耗时(也没必要),我们通常的做法可能是,每完成一个段落的编辑(输入字符、删除字符或者更改字符等)再保存一次,那么Notepad就会在我们编辑段落的时候跟踪段落及其中字符的变化情况,最后一次性将这些变更写到硬盘上。从UoW的模式描述上看,它有点像数据库事务(Transaction),因为它们都具有“提交”和“回滚”的操作。但从语义上讲,它并不能等同于数据库事务。我觉得应该这样理解:我们可以将UoW看成是一个事务对象,但它不是数据库事务,它的事务性体现在能够在一个原子操作中将对象一次性提交给持久化机制,或者如果在提交过程中出现问题,它还能将对象返回到提交前的状态。不仅如此,UoW还具有跟踪领域对象变化的功能,它能够跟踪某一个业务步骤范围内领域对象的变化情况,正如上面的例子中,每个段落的编辑就可以看成是一个业务步骤,那么在这个业务步骤中(编辑段落的过程中),UoW会对领域对象进行跟踪,而在业务步骤完成之时(完成段落编辑之时),UoW就会对跟踪到的变更做一次性提交。
从上面的分析让我们大致了解到,UoW与仓储一样,本身应该是属于Domain Model的,它的设计应该是技术无关的(也就是常说的POCO或者IPOCO),因为它跟踪的是Domain Model中领域对象的变化情况;当然,一个更好的设计应该是使用Separated Interface(PoEAA)模式,将UoW接口与仓储的接口一起设计在Domain Model中。从UoW的实现上来看,NLayerApp采用了Entity Framework的一些特性,并基于Entity Framework的模型,利用T4自动化产生代码。目前我们不要去关心在NLayerApp中是如何使用T4产生这些代码的,我们需要关心的是为什么需要产生这些代码。有关Visual Studio中的模型项目、Domain Specific Language(DSL)以及T4代码自动化生成,我们在此将不作讨论。有兴趣的朋友可以参考我前面的文章《在Visual Studio 2010中使用Modeling Project定制DSL以及自动化代码生成》。以下是NLayerApp中与UoW相关的类关系图:
在了解NLayerApp的UoW执行机制之前,首先让我们了解一下NLayerApp中与UoW相关的三个接口。
- IObjectWithChangeTracker接口
该接口下只定义了一个ObjectChangeTracker的属性,在NLayerApp中,所有的实体都要实现IObjectWithChangeTracker接口,以向外界(主要是UoW和仓储)提供ObjectChangeTracker实例。ObjectChangeTracker的主要功能就是记录当前实体中的状态变化。比如,实体的当前状态、变更前所有属性的原始数据、向集合属性添加的所有对象、从集合属性中删除的所有对象等等。当仓储通过Unit Of Work来注册已变更的实体时,Unit Of Work会使用ObjectChangeTracker所提供的信息来向Entity Framework进行变更注册。 - INotifyPropertyChanged接口
NLayerApp的实体不仅实现了IObjectWithChangeTracker接口,同时还实现了INotifyPropertyChanged接口。实现这个接口的主要目的就是为了在实体的某个属性发生变化时,能及时地将这种变化记录在ObjectChangeTracker中。因此,只要客户程序通过实体的属性来改变实体的状态时,实体本身就会将状态变化记录到ObjectChangeTracker中。 - IRepository接口
IRepository接口是定义在Domain Model层的接口,之所以在此提及,是因为对象的持久化过程是通过仓储完成的,而持久化又离不开UoW。在NLayerApp中,IRepository接口有一个IUnitOfWork的属性,因此所有的仓储都必须实现这个属性,以便Repository能够在UoW中记录对象的变更信息。从NLayerApp的源代码可以看到,其实仓储本身并不负责将实体保存到数据库的这一具体任务,它只是通过IObjectWithChangeTracker接口,将需要保存的对象设置为相应的状态,并向UoW注册对象变更;剩下的与数据库打交道的任务,则是由UoW完成的
通过这些信息我们可以了解到,NLayerApp中的实体都是各自管理自己的变更记录,称之为“自跟踪实体”(Self-Tracking Entities,STE)。其实从DDD的角度来看,STE并不是一个很好的设计,因为它给Domain Model带来了太多技术关注点。例如在实现STE的时候,当你向Customer添加一个Order时,你需要首先判断Customer的ObjectChangeTracker中是否已经将该Order标记为“删除”状态了,如果是这样的话,那么你需要将这个Order从ObjectChangeTracker的“删除”列表中移去。类似这样的业务逻辑本不应该放在Domain Model中。此外,NLayerApp为了迎合Entity Framework的需求,所实现的STE也并非纯粹的与技术无关的。UoW的实现也是如此,比如在上面的类图中,我们可以很明显地看到,MainModuleUnitOfWork是ObjectContext的子类。
现在我们将思路串联起来,以修改Customer为例,从整个架构服务端的最上层(Distributed Service层)开始,看看Unit Of Work与仓储是如何协作的。
1、DistributedServices.MainModule项目:MainModuleService类通过使用位于应用层的CustomerManagementService实现Customer信息的变更:
public void ChangeCustomer(Customer customer) { try { //Resolve root dependency and perform operation ICustomerManagementService customerService = IoCFactory .Instance .CurrentContainer.Resolve<ICustomerManagementService>(); customerService.ChangeCustomer(customer); } catch (ArgumentNullException ex) { // ...... } }
上述代码通过IoCFactory从IoC容器中获得ICustomerManagementService的具体实现,有关NLayerApp中IoC容器的实现,请参考前一篇文章。
2、Application.MainModule项目:CustomerManagementService类实现了ICustomerManagementService接口,同时实现了ChangeCustomer方法。在该方法中,首先通过CustomerRepository的UnitOfWork属性获得UoW,然后调用仓储的Modify方法以将要更改的Customer实体注册到UoW中,同时改变了Customer实体的状态。最后,使用UoW的CommitAndRefreshChanges方法将变更的实体对象提交到数据库:
public void ChangeCustomer(Customer customer) { if (customer == (Customer)null) throw new ArgumentNullException("customer"); IUnitOfWork unitOfWork = _customerRepository.UnitOfWork as IUnitOfWork; _customerRepository.Modify(customer); unitOfWork.CommitAndRefreshChanges(); }
值得一提的是,在CustomerManagementService中,CustomerRepository以构造器注入的方式获得实例化的:
/// <summary> /// Create new instance /// </summary> /// <param name="customerRepository">Customer repository dependency, /// intented to be resolved with dependency injection</param> /// <param name="countryRepository">Country repository dependency, /// intended to be resolved with dependency injection</param> public CustomerManagementService(ICustomerRepository customerRepository, ICountryRepository countryRepository) { if (customerRepository == (ICustomerRepository)null) throw new ArgumentNullException("customerRepository"); if (countryRepository == (ICountryRepository)null) throw new ArgumentNullException("countryRepository"); _customerRepository = customerRepository; _countryRepository = countryRepository; }
3、Infrastructure.Data.Core项目:Repository类的Modify方法首先将当前状态不是Deleted的实体设置为“Modified”,同时在UoW中,通过RegisterChanges调用以向UoW注册该实体:
public virtual void Modify(TEntity item) { //check arguments if (item == (TEntity)null) throw new ArgumentNullException("item", Resources.Messages.exception_ItemArgumentIsNull); //Set modifed state if change tracker is enabled and state is not deleted if (item.ChangeTracker != null && ((item.ChangeTracker.State & ObjectState.Deleted) != ObjectState.Deleted) ) { item.MarkAsModified(); } //apply changes for item object _CurrentUoW.RegisterChanges(item); _TraceManager.TraceInfo( string.Format(CultureInfo.InvariantCulture, Resources.Messages.trace_AppliedChangedItemRepository, typeof(TEntity).Name)); }
4、Infrastructure.Data.MainModule项目:MainModuleUnitOfWork类的RegisterChanges方法简单地利用Entity Framework所提供的机制,向Entity Framework注册对象状态变更。这是Entity Framework技术实现的细节内容,我们在此也不去深入分析其中的实现方式了:
public void RegisterChanges<TEntity>(TEntity item) where TEntity : class, IObjectWithChangeTracker { this.CreateObjectSet<TEntity>().ApplyChanges(item); }
5、Infrastructure.Data.MainModule项目:MainModuleUnitOfWork类的CommitAndRefreshChanges方法通过Entity Framework将变更提交到数据库,同时将实体对象的状态设置为“未更改”:
public void CommitAndRefreshChanges() { try { //Default option is DetectChangesBeforeSave base.SaveChanges(); //accept all changes in STE entities attached in context IEnumerable<IObjectWithChangeTracker> steEntities = (from entry in this.ObjectStateManager .GetObjectStateEntries(~EntityState.Detached) where entry.Entity != null && (entry.Entity as IObjectWithChangeTracker != null) select entry.Entity as IObjectWithChangeTracker); steEntities.ToList().ForEach(ste => ste.MarkAsUnchanged()); } catch (OptimisticConcurrencyException ex) { //...... } }
整个执行过程我们可以使用下面的序列图来表示:
NLayerApp中的Unit Of Work我们先介绍到这里,有疑问的朋友可以以评论的方式交流。
仓储的具体实现
NLayerApp中的仓储实现也是基础结构层(数据访问部分)的一个重要组件,这一点与DDD的经典架构风格是相符的。因为从理论上讲,仓储的具体实现需要依赖于外部系统,而这部分内容是不能暴露给Domain Model层的,也就是我们平时所说的,需要做到Persistence Ignorance。NLayerApp首先为所有实体(确切地说应该是聚合根)设计了一个通用的泛型仓储,你可以在Infrastructure.Data.Core项目中找到这个泛型仓储的源代码,它实现了一个仓储应具有的所有基本功能,比如添加、删除、修改实体对象以及基于规约的一些查询操作等;然后,针对某些聚合根,NLayerApp会根据项目的实际需求,在仓储中实现一些特定的操作。比如:CustomerRepository继承于Repository这个通用仓储,同时实现了ICustomerRepository接口,以向外界提供通过规约(Specification)来查找Customer信息的功能。这样的设计在一定程度上做到了关注点分离,比如当我们对实体进行通用的仓储操作时,我们只需要获得IRepository接口的具体实现即可,而无需使用ICustomerRepository来获得与Customer有关的仓储实现。有关ICustomerRepository与关注点分离的相关内容,我将在下一讲(领域模型层)进行讲解。
以下是NLayerApp中仓储的类关系图,在此贴出以供读者参考。
NLayerApp的仓储实现也使用了不少与Entity Framework相关的技术细节,比如ObjectSet等,这些都是具体技术实现上的内容,在此就不多作介绍了。有兴趣的读者请参考与Entity Framework技术相关的资料文档。
NLayerApp的数据模型
NLayerApp使用Entity Framework的ADO.NET Entity Data Model设计器来设计数据模型,这使得我们能够对整个Domain Model的对象结构有一个很直观的认识。该数据模型位于Infrastructure.Data.MainModule项目下,直接双击MainModuleDataModel.edmx就可以在设计器中打开,对象结构及其之间的关系就能很清楚地展现在你面前。你会发现,其实在这个数据模型的后台代码文件中,除了一些注释以外,并没有任何实质性内容,这是因为NLayerApp仅仅是利用这个设计器来设计数据模型,而真正的Domain Model的代码则会在Domain Model层中,根据该数据模型,利用T4进行自动化生成,详情请见Domain.MainModule.Entities项目。这也使得我们会去思考这样一个纠结的问题:Entity Framework为我们提供的,到底是一个面向数据库设计的数据模型,还是面向领域驱动的领域模型?或许在实际应用中,我们更多地是将其放在ORM的位置上,于是Entity Data Model就变成了位于Domain Model实体对象与数据库之间的行数据入口(Row Data Gateway,PoEAA)。之前我对于基于Entity Framework的领域驱动设计实践也写过一些文章,读者朋友可以参考《领域驱动设计系列文章汇总》。
总结
本文对NLayerApp的基础结构层(数据访问部分),尤其是Unit Of Work的实现进行了分析与介绍;下一讲开始,我们将一起学习NLayerApp的Domain Model部分。