上篇文章讲解了NLayerApp案例的基础结构层(Cross-Cutting部分),现在,让我们继续解读NLayerApp的基础结构层(数据访问部分)。NLayerApp的基础结构层(数据访问部分)包含如下内容:Unit Of Work(PoEAA)、仓储的具体实现、NLayerApp的数据模型以及与测试相关的类。下面,我们将对前三个部分进行讨论,与测试相关的内容,我打算最后单独一章进行介绍。
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相关的三个接口。
通过这些信息我们可以了解到,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信息的变更:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
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方法将变更的实体对象提交到数据库:
1
2
3
4
5
6
7
8
9
10
11
12
|
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以构造器注入的方式获得实例化的:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
/// <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注册该实体:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
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技术实现的细节内容,我们在此也不去深入分析其中的实现方式了:
1
2
3
4
5
|
public
void
RegisterChanges<TEntity>(TEntity item)
where
TEntity :
class
, IObjectWithChangeTracker
{
this
.CreateObjectSet<TEntity>().ApplyChanges(item);
}
|
5、Infrastructure.Data.MainModule项目:MainModuleUnitOfWork类的CommitAndRefreshChanges方法通过Entity Framework将变更提交到数据库,同时将实体对象的状态设置为“未更改”:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
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使用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部分。