UnitOfWork以及其在ABP中的應用


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 uow
                invocation.Proceed();
                return;
            }

            var unitOfWorkAttr = UnitOfWorkAttribute.GetUnitOfWorkAttributeOrNull(invocation.MethodInvocationTarget);
            if (unitOfWorkAttr == null || unitOfWorkAttr.IsDisabled)
            {
                //No need to a uow
                invocation.Proceed();
                return;
            }

            //No current uow, run a new one
            PerformUow(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 denant
                        user = 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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM