徐州网站关键词排名,网站开发是用模版还是纯手打,可玩儿小程序代理,用html做网站的背景图怎么弄概述在上一篇 《如何运用领域驱动设计 - 存储库》 的文章中#xff0c;我们讲述了有关仓储的概念和使用规范。仓储为聚合提供了持久化到本地的功能#xff0c;但是在持久化的过程中#xff0c;有时一个聚合根中的各个领域对象会分散到不同的数据库表里面#xff1b;又或者是… 概述在上一篇 《如何运用领域驱动设计 - 存储库》 的文章中我们讲述了有关仓储的概念和使用规范。仓储为聚合提供了持久化到本地的功能但是在持久化的过程中有时一个聚合根中的各个领域对象会分散到不同的数据库表里面又或者是一个用例操作需要操作多个仓储而这些操作都应该要么同时成功要么同时失败因此就需要为这一系列操作提供事务的支持而事务管理就是由工作单元来提供的。在上一篇中可能已经提到了工作单元但是仅仅是一笔带过现在我们就来详细的探究该如何更好的来实现工作单元。文章的代码片段都使用的是C#,案例项目也是基于 DotNet Core 平台。直接看东西在上一篇文章中已经为大家提供了一个Github的Demo。如果已经下载过该Demo的同学您现在直接进行Pull就可以获得最新的版本了如果还没有下载该Demo的同学也可以戳下方的跳转链接获取。GitHub 地址点击直达哟在这里我们可以先来看一下该项目的应用代码是什么样子复制代码[HttpPost]
public ActionResultstring Add()
{//使用仓储来处理聚合_itineraryRepository.Add(new Itinerary(奥特曼,赛文奥特曼,杰克奥特曼,佐菲奥特曼,泰罗奥特曼));_itineraryRepository.Add(new Itinerary(盖亚奥特曼,戴拿奥特曼,阿古茹奥特曼,迪迦奥特曼, ));return success;
}[HttpGet]
public ActionResultlong Get()
{var count _itineraryRepository.GetCount();return count;
}这是在Aspnet Core的Controller中的代码也就是对外提供的Api。可以看到我们仅仅只是通过仓储的调用就完成了所有的操作。ps:原谅我该演示api没有遵循restful风格(▽)还有就是那些奥特曼。。。。您可能会说这里没有做操作那肯定是在 ItineraryRepository 里面做了手脚。好吧下面我们来看看该仓储的实现。复制代码public class ItineraryRepository: EFRepositoryUowAppDbContext, Itinerary, Guid
{public void Add(Itinerary itinerary) DbContext.SetItinerary().Add(itinerary);
}是的它也只有这么一点点代码。而作为后期的业务扩展和维护我们只需要完善我们的Itinerary聚合为它扩展行为和增加实体或值对象以及ItineraryRepository仓储为它添加对外检索意图的方法就可以了。这种做法的好处可能您很快就能发现在我们代码中处处都是关于领域对象的操作尽可能的避免其它基础构建或功能支持组件来干扰程序。除了代码量的减少之外它也让可读性有着明显的提高如果在此基础上能够构建出明确而干净的聚合根那么您的程序将具备更高的可扩展性。好吧回到我们今天的主题工作单元。其实上面的代码就是对仓储中工作单元的巧妙运用它其实在后面默默的支持着程序的正常运转这是在调用层面上我们完全感觉不到它的存在而已。下面就为您介绍它是怎么工作和实现的。什么是工作单元按照国际管理呢这一章节都是解读有关原著《领域驱动设计:软件核心复杂性应对之道》 中的解释。但是有关工作单元的概念在书里并没有被明确的提及到。所以为了证明我们确确实实是在前人的基础理念上来实践而不是胡编乱造自己随便弄了一个概念出来。我特地去找了另外一本较为权威的领域驱动设计教材《领域驱动设计模式、原理与实践》 。在该书中对工作单元的解释如下事务管理主要与应用程序服务层有关。存储库只与使用聚合根的单一集合的管理有关而业务用例可能会造成对多个类型聚合的更新。事务管理是由工作单元处理的。工作单元模式的作用是保持追踪业务任务期间聚合的所有变化。一旦所有的变化都已发生则之后工作单元会协调事务中持久化存储的更新。如果在将变更提交到数据存储的中途出现了问题那么要确保不损坏数据完整性的话就要回滚所有的变更以确保数据保持有效的状态。其实上文的话真的很好理解相对于原著而言(y▽,)╭ 。首先我们可以得到的第一个结论事务管理其实是应用服务层干的事。第二个结论事务的协调管理都是由工作单元来负责的所以我们千万不能因为工作单元和仓储有联系就将它放置在领域层里面事务的提供往往是由数据库管理程序来提供的而这一类组件我们一般将它们放置在基础构架层而领域层可以依赖于基础构架层所以千万要注意保持您的领域层足够干净不要让其它的东西干扰它也更不要将事务处理这类东西放到了您的领域层来。这一点您会在后期MiCake米蛋糕的使用中看到详细的案例。如何实现工作单元实现工作单元就是要实现仓储中的事务操作。您可能已经看到过有些实现Repository的框架它的写法是注入一个unitOfWork,然后从uow中提取一个仓储然后再用仓储来完成聚合根的持久化操作。类似的代码就像这样复制代码var yourRepository uow.GetRepositoryyourRepository();
yourRepository.Add(yourEntity);uow.Commit();这样做没有一点点的问题而且是对工作单元和仓储模式的完美实现。uow工作单元中维持了一个事务从该工作单元中创建的每一个仓储都可以获得该事务仓储完成了自己的操作之后工作单元使用Commit方法告诉事务管理器该事务完成。复制代码夏目去参加了妖怪的聚会一回到家猫咪老师就发现了它沾染了妖怪的味道当仓储的操作沾染上了工作单元的事务它也就受到了事务的管理如果您喜欢这种实现模式可以参考 threenine的Threenine.Data项目。懒的模式其实在刚开始为 MiCake(米蛋糕) 选取工作单元实现方案的时候我也打算采用这种方式。但是在思考了一天之后我还是放弃了。因为我发现这种模式在完成每一次仓储操作的时候必须要从工作单元中去获取。在Aspnet Core中不得不在Controller中注入工作单元对象然后再从该对象里面去获取仓储。这显然削弱了依赖注入所为我们提供的依赖阅读性原本在构造函数中我能看出我需要注入的是A仓储但是现在我看到的只有工作单元。其实最重要的一点就是我太懒啦 o_o ....。为什么每次都要去多写一个uow.GetXXXXX()。每使用一个仓储就要多写一次获取语句我就不能好好的只使用仓储吗所以在这个想法的强烈刺激下我选取了另外的实现方法。接下来就让我们来实现最开始演示代码中的工作单元吧。哦对了忘记说了无论是演示的Github Demo还是本次的博文我们都选取了Entity Framework Core来作为数据持久组件。所以有些小伙伴会说那我使用Dapper或者原生的ADO怎么办其实思路都是一样的您也可以在看了EFCore的版本后自己写出对应的工作单元版本。如果有机会的话欢迎在Github的Demo上直接添加就可以提交供更多的同学参考啦。实现思路找出当前数据库持久组件中具有事务特征的对象比如在EF中就是DbContext创建一个容器去容纳这些对象工作单元就是该容器的实现它掌管了这些事务对象并对外公布了提交事务的方法工作单元管理器负责了对工作单元的创建工作脑袋里有了这些还比较模糊的交互对象之后我们可以来想一下一个仓储完成添加聚合根的操作是怎么样的在访问该API之前使用工作单元管理器创建一个工作单元访问API中的仓储时候构造一个事务特征对象并开启一个事务事务开启完成之后将该事务特征对象尝试放入到当前工作单元仓储事务操作完成后调用工作单元的提交方法完成事务的提交保证仓储的数据一致。事务完成后释放上面的各个对象虽然步骤好像有5步但总结下来就是将具有事务的对象放置到工作单元中让它去负责提交。对就是这么简单该方法与上面那种从工作单元中获取仓储的方法想法它是往工作单元中提交。所以我们此时可以构造出一个伪代码出来大致理解它的实现复制代码 //1、使用工作单元管理器创建一个工作单元using (var uow unitOfWorkManager.Create()){//2、构造事务特征对象开启事务并注册到工作单元RegisteTransactonFeature(DbContext);//3、执行仓储中的内容DbContext.SetItinerary().Add(itinerary)//4、工作单元保存提交uow.SaveChanges();//5、dispose}至少到目前我们可以抽象出上面的各个对象了。您也可以先自己尝试着想一想每个对象接口应该实现什么功能方法。复制代码//首先是事务特征对象它提供了事务的基本Commit和Rollback方法
public interface ITransactionFeature
{public bool IsCommit { get; }public bool IsRollback { get; }void Commit();Task CommitAsync(CancellationToken cancellationToken default);void Rollback();Task RollbackAsync(CancellationToken cancellationToken default);
}//然后是事务特征容器它具有增加删除事务特征对象的方法
public interface ITransactionFeatureContainer
{void RegisteTranasctionFeature(string key, ITransactionFeature TransactionFeature);ITransactionFeature GetOrAddTransactionFeature(string key, ITransactionFeature TransactionFeature);ITransactionFeature GetTransactionFeature(string key);void RemoveTransaction(string key);
}//接下来是工作单元它实现了事务特征容器并且对外提供提交的方法
public interface IUnitOfWork : ITransactionFeatureContainer
{Guid ID { get; }bool IsDisposed { get; }void SaveChanges();Task SaveChangesAsync(CancellationToken cancellationToken default);void Rollback();Task RollbackAsync(CancellationToken cancellationToken default);
}//最后是工作单元管理器它提供了创建工作单元的方法
public interface IUnitOfWorkManager : IUnitOfWokrProvider, IDisposable
{IUnitOfWork Create();
}落地代码在构建出接口之后我们就可以写出具体的实现类了。首先是实现工作单元UnitOfWork对象。由于具体代码实现较多讲解部分只选取了核心部分完整代码可以参考Github的项目复制代码public class UnitOfWork : IUnitOfWork
{private readonly Dictionarystring, ITransactionFeature _transactionFeatures;public UnitOfWork(){_transactionFeatures new Dictionarystring, ITransactionFeature();}//往容器中添加事物特征对象public virtual ITransactionFeature GetOrAddTransactionFeature([NotNull]string key,[NotNull] ITransactionFeature transcationFeature){if (_transactionFeatures.ContainsKey(key))return _transactionFeatures.GetValueOrDefault(key);_transactionFeatures.Add(key, transcationFeature);return transcationFeature;}//对外提供的保存方法执行该方法时调用容器内所有事物特征对象的Commit方法public virtual void SaveChanges(){foreach (var transactionFeature in _transactionFeatures.Values){transactionFeature.Commit();}}
}接下来就是与ORM框架关联最深的事务特征对象的实现了由于我们选取了EF所以此处应该实现EF版本的事务特征对象复制代码public class EFTransactionFeature : ITransactionFeature
{private IDbContextTransaction _dbContextTransaction;private DbContext _dbContext;public EFTransactionFeature(DbContext dbContext){_dbContext dbContext;}//设置事务public void SetTransaction(IDbContextTransaction dbContextTransaction){_isOpenTransaction true;_dbContextTransaction dbContextTransaction;}public void Commit(){if (IsCommit)return;IsCommit true;//EF 事务的提交_dbContext.SaveChanges();_dbContextTransaction?.Commit();}
}建立好了这两个对象之后其实我们只需要一个流转过程就可以实现工作单元了。这个流程就是将事务特征对象添加到工作单元中但是我们应该在什么时候将它添加进去呢看过第一版Github代码的小伙伴可能知道在仓储调用的时候就可以完成该操作。当时在第一版中我们的实现代码是这样的复制代码public class EFRepository
{protected IUnitOfWorkManager UnitOfWorkManager { get; private set; }protected DbContext DbContext { get; private set; }public EFRepository(IUnitOfWorkManager unitOfWorkManager, DbContext dbContext){UnitOfWorkManager unitOfWorkManager;DbContext dbContext;}public void Add(TAggregateRoot aggregateRoot){RegistUnitOfWork(DbContext);DbContext.SetTAggregateRoot().Add(aggregateRoot);}private void RegistUnitOfWork(DbContext dbContext){string key $EFTransactionFeature - {dbContext.ContextId.InstanceId.ToString()};unitOfWork.ResigtedTransactionFeature(key, new EFTransactionFeature(DbContext));}
}在每一次进行仓储操作的时候都调用了一个RegistUnitOfWork的方法来完成事务特征对象和工作单元的流转工作。但是很快您就能发现问题EFRepository是我们实现的一个基类以后所有的仓储操作都继承该类来完成操作那不是每扩展一个方法我都要在该方法中写一句注册代码如果我忘记写了怎么办。还有一点该注册过程并没有开启一个事务那么事务是怎么来的呢那么怎么才能避免用户每一次都要去显示调用注册呢而是让用户在不知不觉中就完成了该操作。所以我们得思考在每一个方法中用户都一定会写的代码是什么然后在该代码上下手。可能您已经想到了DbContext是的每一个方法里用户都会去写DbContext所以我们可以在他获取DbContext的时候就完成注册操作。所以优化后的代码就是这样的复制代码public class EFRepository
{public virtual TDbContext DbContext{get _dbContextFactory.CreateDbContext();}public void Add(TAggregateRoot aggregateRoot){DbContext.SetTAggregateRoot().Add(aggregateRoot);}
}而该_dbContextFactory的实现就更简单了他要完成的任务就是注册到工作单元并且开启事务。复制代码
internal class UowDbContextFactoryTDbContext
{private readonly IUnitOfWorkManager _uowManager;public UowDbContextFactory(IUnitOfWorkManager uowManager){_uowManager uowManager;}public TDbContext CreateDbContext(){AddDbTransactionFeatureToUow(currentUow, DbContext);return wantedDbContext;}private void AddDbTransactionFeatureToUow(IUnitOfWork uow, TDbContext dbContext){string key $EFCore - {dbContext.ContextId.InstanceId.ToString()};var efFeature uow.GetOrAddTransactionFeature(key, new EFTransactionFeature(dbContext));if (IsFeatureNeedOpenTransaction(uow, efFeature)){var dbcontextTransaction dbContext.Database.BeginTransaction();efFeature.SetTransaction(dbcontextTransaction);}}private bool IsFeatureNeedOpenTransaction(IUnitOfWork uow, EFTransactionFeature efFeature){return !efFeature.IsOpenTransaction;}
}dbContext.Database.BeginTransaction是EF为我们提供的手动开启事务的方法。如果您尝试实现另外ORM版本的工作单元想一下在该ORM中是怎么开启的事务。此时我们就已经实现了工作单元的流转了那么还有一个问题就是我们怎么默认去实现一个工作单元而不是每一次都需要手动去开启并提交。AspNet Core为我们提供了很好的拦截方法。第一种方法 我们可以在中间件中完成因为所有的请求都要穿过中间件我们可以在方法到API之前就开启事务等API访问结束后就提交事务。第二种方法 通过IActionFilter等周期接口来完成。本案例选取了第一种实现方法您也可以根据您自己的爱好选取自己的实现方式。缺陷到这里我们已经实现了像上面Demo版本的工作单元但是该工作单元其实还有许多特性没有实现一个业务操作一个API中没有创建多个工作单元的能力目前事务的操作来源于EF Core的支持如果项目存在多种数据访问方式比如一个EF一个ADO它们之间如何依靠工作单元来完成事务没有识别什么时候需要开启工作单元如果一个操作仅仅需要获取数据其实我们是不需要开启工作单元的不过如果您的项目仅仅使用了一种ORM框架并且只需要开启一个工作单元那么可以尝试使用该实现。在实现MiCake真正的工作单元中我尝试了很多方法来解决上面的问题。在后面的文章中您也会看到MiCake真正的工作单元。附上一个当时写工作单元的手记︶↗总结本来这篇文章不打算写在《如何运用领域驱动设计》这个系列的但是后来纠结了一下还是纳入了该系列。由于该篇文章是实现工作单元的所以代码量就比较大希望不会给您造成阅读上的困难。下一篇的文章是一个谈了很久的问题————持久化值对象现在终于是时候该解决它了。在本次Demo中您看到的聚合根Itinerary所有的属性都是string很显然这是不符合常理的所以在下一次就要让它成为真正的领域对象。ps:改成真正的领域对象后感觉都可以单体DDD应用落地了呢。︶↗醒醒少年。为了您不错过下一篇文章的内容您也可也点击博客园右上角的关注这样就能及时收到更新了哟。