Unit Of Work(UoW)模式在企业应用架构中被广泛使用,它能够将Domain Model中对象状态的变化收集起来,并在适当的时候在同一数据库连接和事务处理上下文中一次性将对象的变更提交到数据中。
从字面上我们可以我们可以把UnitOfWork叫做工作单元,从概念上它是协助代码块的事务。为什么我们需要用UnitOfWork?有人说EF不是的DbContext的SaveChanges不就有提交变更数据的功能吗?为什么还要多一层封装?是的,如果我们项目只是用EF的话,项目又会经常变更,不用考虑那么多我们可以直接用EF,但是如果我们在支持EF的同时还需要支持Redis、NHibernate或MongoDB呢?我们怎么做统一的事务管理?所以封装一个UnitOfWork是有必要的。类似的Repository也是一样,仓储Repository的功能其实就是EF的DbSet<T>,但是我们的数据库访问技术可能会改变,所以我们需要提供一层封装,来隔离领域层或应用层对数据访问层的依赖。那么ABP是怎么定义UnitOfWork的呢?
public interface IUnitOfWork : IActiveUnitOfWork, IUnitOfWorkCompleteHandle{/// <summary>/// Begins the unit of work with given options./// 开始 unit of work的一些配置UnitOfWorkOptions,主要是事务的级别,超时时间,配置文件等/// </summary>/// <param name="options">Unit of work options</param>void Begin(UnitOfWorkOptions options);}public interface IUnitOfWorkCompleteHandle : IDisposable{/// <summary>/// Completes this unit of work./// It saves all changes and commit transaction if exists./// 统一事务提交/// </summary>void Complete();/// <summary>/// Completes this unit of work./// It saves all changes and commit transaction if exists./// 异步的Complete方法/// </summary>Task CompleteAsync();}
从接口的定义来看,UnitOfWork主要涉及到事务的提交,回滚操作这边没有再定义一个方法,因为作者用的是TransactionScope,失败了会自动回滚。当然有定义了Dispose方法。现在我们来看下UnitOfWork的实现方法,抽象类UnitOfWorkBase,我删除了一些跟本文无关的代码,方便阅读。
public abstract class UnitOfWorkBase : IUnitOfWork{public UnitOfWorkOptions Options { get; private set; }/// <summary>/// 开始UnitOfWork的一些配置,和事务的初始化/// </summary>/// <param name="options"></param>public void Begin(UnitOfWorkOptions options){if (options == null){throw new ArgumentNullException("options");}PreventMultipleBegin();Options = options; //TODO: Do not set options like that!SetFilters(options.FilterOverrides);BeginUow();}/// <summary>/// 事务的提交,异常的捕获/// </summary>public void Complete(){PreventMultipleComplete();try{CompleteUow();_succeed = true;OnCompleted();}catch (Exception ex){_exception = ex;throw;}}/// <summary>/// 结束事务,失败就回滚/// </summary>public void Dispose(){if (IsDisposed){return;}IsDisposed = true;if (!_succeed){OnFailed(_exception);}DisposeUow();OnDisposed();}}
我们知道UnitOfWorkBase是抽象类,对于不同的数据访问技术方案我们要定义不用的工作单元实现类,比如EF和NHibernate的事务实现机制是不一样的,这里我们看下EfUnitOfWork
public class EfUnitOfWork : UnitOfWorkBase, ITransientDependency{private readonly IDictionary<Type, DbContext> _activeDbContexts;private readonly IIocResolver _iocResolver;private TransactionScope _transaction;/// <summary>/// Creates a new <see cref="EfUnitOfWork"/>./// </summary>public EfUnitOfWork(IIocResolver iocResolver, IUnitOfWorkDefaultOptions defaultOptions): base(defaultOptions){_iocResolver = iocResolver;_activeDbContexts = new Dictionary<Type, DbContext>();}protected override void BeginUow(){if (Options.IsTransactional == true){var transactionOptions = new TransactionOptions{IsolationLevel = Options.IsolationLevel.GetValueOrDefault(IsolationLevel.ReadUncommitted),};if (Options.Timeout.HasValue){transactionOptions.Timeout = Options.Timeout.Value;}_transaction = new TransactionScope(TransactionScopeOption.Required,transactionOptions,Options.AsyncFlowOption.GetValueOrDefault(TransactionScopeAsyncFlowOption.Enabled));}}public override void SaveChanges(){_activeDbContexts.Values.ForEach(SaveChangesInDbContext);}
...
上面已经定义了UnitOfWork接口和实现方法,那我们改怎么使用呢?一般的我们的使用方式是这样的,下面的场景是模拟银行转账功能,从一个账户扣钱和另一个账户加钱。下面是领域层定义的账户转账服务,我们在整个操作实现完后调用 _unitOfWork.Commit()进行提交,在领域服务构造函数注入UnitOfWork。
// 账号转账领域服务类public class AccountService{private readonly IAccountRepository _productRepository;private readonly IUnitOfWork _unitOfWork;public AccountService(IAccountRepository productRepository, IUnitOfWork unitOfWork){_productRepository = productRepository;_unitOfWork = unitOfWork; }public void Transfer(Account from, Account to, decimal amount){if (from.Balance >= amount){from.Balance -= amount;to.Balance += amount;_productRepository.Save(from);_productRepository.Save(to);_unitOfWork.Commit();}} }
这样的设计简单易懂,但是我们每个提交都要引用UnitOfWork会比较麻烦,那么有没有更好的设计思路呢?ABP的设计思想还是比较值得借鉴的。ABP的UnitOfWork的设计思路还是沿用作者最喜欢的切面编程,何为切面编程:通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。也就是AOP技术,ABP作者用的是Castle Windsor来实现的。一般的我们需要两步,1、继承IInterceptor接口重写Intercept方法,这样我们就可以实现动态拦截方法了,2、那么我们到底怎么才能动态代理要拦截的方法呢?我们可以继承Attribute,自定义UnitOfWorkAttribute。可能你现在还不明白,那么我们来看下具体代码吧。
internal class UnitOfWorkInterceptor : IInterceptor{private readonly IUnitOfWorkManager _unitOfWorkManager;public UnitOfWorkInterceptor(IUnitOfWorkManager unitOfWorkManager){_unitOfWorkManager = unitOfWorkManager;}public void Intercept(IInvocation invocation){if (_unitOfWorkManager.Current != null){//Continue with current uowinvocation.Proceed();return;}var unitOfWorkAttr = UnitOfWorkAttribute.GetUnitOfWorkAttributeOrNull(invocation.MethodInvocationTarget);if (unitOfWorkAttr == null || unitOfWorkAttr.IsDisabled){//No need to a uowinvocation.Proceed();return;}//No current uow, run a new onePerformUow(invocation, unitOfWorkAttr.CreateOptions());}
对于Castle Windsor我们只需要像上面的UnitOfWorkInterceptor就是继承IInterceptor重写Intercept就可以实现动态代理啦。下面来看下自定义的UnitOfWorkAttribute。
[AttributeUsage(AttributeTargets.Method)]public class UnitOfWorkAttribute : Attribute{/// <summary>/// Is this UOW transactional?/// Uses default value if not supplied./// </summary>public bool? IsTransactional { get; private set; }/// <summary>/// Timeout of UOW As milliseconds./// Uses default value if not supplied./// </summary>public TimeSpan? Timeout { get; private set; }
好了,定义了UnitOfWorkAttribute,那么我们怎么让它和UnitOfWorkInterceptor结合起来对代码进行动态拦截呢?
[UnitOfWork]public virtual async Task<AbpLoginResult> LoginAsync(string userNameOrEmailAddress, string plainPassword, string tenancyName = null){if (userNameOrEmailAddress.IsNullOrEmpty()){throw new ArgumentNullException("userNameOrEmailAddress");}if (plainPassword.IsNullOrEmpty()){throw new ArgumentNullException("plainPassword");}using (_unitOfWorkManager.Current.DisableFilter(AbpDataFilters.MayHaveTenant)){TUser user;if (!_multiTenancyConfig.IsEnabled){using (_unitOfWorkManager.Current.EnableFilter(AbpDataFilters.MayHaveTenant)){//Log in with default denantuser = await FindByNameOrEmailAsync(userNameOrEmailAddress);if (user == null){return new AbpLoginResult(AbpLoginResultType.InvalidUserNameOrEmailAddress);}}}
上面代码是利用Attribute的特性对方法进行标识,这是第一步,现在我们已经对要拦截的代码标识了,那么我们是怎么知道它要被拦截的呢?
internal static class UnitOfWorkRegistrar{/// <summary>/// Initializes the registerer./// </summary>sssss/// <param name="iocManager">IOC manager</param>public static void Initialize(IIocManager iocManager){iocManager.IocContainer.Kernel.ComponentRegistered += ComponentRegistered;}/// <summary>/// 拦截注册事件 /// </summary>/// <param name="key"></param>/// <param name="handler"></param>private static void ComponentRegistered(string key, IHandler handler){if (UnitOfWorkHelper.IsConventionalUowClass(handler.ComponentModel.Implementation)){//判断如果是IRepository和IApplicationService,就注册动态代理 Intercept all methods of all repositories.handler.ComponentModel.Interceptors.Add(new InterceptorReference(typeof(UnitOfWorkInterceptor)));}else if (handler.ComponentModel.Implementation.GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic).Any(UnitOfWorkHelper.HasUnitOfWorkAttribute)){//判断如果是被标识了UnitOfWork attribute的就注册动态代理 Intercept all methods of classes those have at least one method that has UnitOfWork attribute.//TODO: Intecept only UnitOfWork methods, not other methods!handler.ComponentModel.Interceptors.Add(new InterceptorReference(typeof(UnitOfWorkInterceptor)));}}}
请认真看上面的方法ComponentRegistered的代码,UnitOfWorkHelper.HasUnitOfWorkAttribute就是判断是否是UnitOfWorkAttribute。
public static bool HasUnitOfWorkAttribute(MemberInfo methodInfo){return methodInfo.IsDefined(typeof(UnitOfWorkAttribute), true);}
你可能会问UnitOfWorkRegistrar的ComponentRegistered方法是什么时候执行的?那么你可以参考下我之前写的Castle Windsor常用介绍以及其在ABP项目的应用介绍 ,关于UnitOfWorkAttribute 是怎么执行的可以参考ABP之模块分析
那么到此我们被标识[UnitOfWork]的登录方法LoginAsync和所有的repositories仓储一旦被执行到就会被拦截,执行我们的代理类。本来这篇文章还想说说仓储的,但是因为篇幅可能会有点长,那就放在下次总结吧,至此UnitOfWork也就总结到此了。
参考文章:
http://www.cnblogs.com/daxnet/archive/2011/06/03/2071931.html
http://www.cnblogs.com/zhili/p/UnitOfWork.html
http://www.cnblogs.com/xishuai/p/3750154.html