統一工做單元是一個比較重要的基礎設施組件,它負責管理整個業務流程當中涉及到的數據庫事務,一旦某個環節出現異常自動進行回滾處理。html
在 ABP vNext 框架當中,工做單元被獨立出來做爲一個單獨的模塊(Volo.Abp.Uow)。你能夠根據本身的須要,來決定是否使用統一工做單元。數據庫
整個 Volo.Abp.Uow 項目的結構以下,從下圖仍是能夠看到咱們的老朋友 IUnitOfWorkManager
和 IUnitOfWork
,不過也多了一些新東西。看一個模塊的功能,首先從它的 Module
入手,咱們先看一下 AbpUnitofWorkModule
裏面的實現。api
打開 AbpUnitOfWorkModule
裏面的代碼,發現仍是有點失望,裏面就一個服務註冊完成事件。框架
public override void PreConfigureServices(ServiceConfigurationContext context) { context.Services.OnRegistred(UnitOfWorkInterceptorRegistrar.RegisterIfNeeded); }
這裏的結構和以前看的 審計日誌 模塊相似,就是註冊攔截器的做用,沒有其餘特別的操做。異步
繼續跟進代碼,其實現是經過 UnitOfWorkHelper
來肯定哪些類型應該集成 UnitOfWork
組件。async
public static void RegisterIfNeeded(IOnServiceRegistredContext context) { // 根據回調傳入的 context 綁定的實現類型,判斷是否應該爲該類型註冊 UnitOfWorkInterceptor 攔截器。 if (UnitOfWorkHelper.IsUnitOfWorkType(context.ImplementationType.GetTypeInfo())) { context.Interceptors.TryAdd<UnitOfWorkInterceptor>(); } }
繼續分析 UnitOfWorkHelper
內部的代碼,第一種狀況則是實現類型 (implementationType
) 或類型的任一方法標註了 UnitOfWork
特性的話,都會爲其註冊工做單元攔截器。ide
第二種狀況則是 ABP vNext 爲咱們提供了一個新的 IUnitOfWorkEnabled
標識接口。只要繼承了該接口的實現,都會被視爲須要工做單元組件,會在系統啓動的時候,自動爲它綁定攔截器。函數
public static bool IsUnitOfWorkType(TypeInfo implementationType) { // 第一種方式,即判斷具體類型與其方法是否標註了 UnitOfWork 特性。 if (HasUnitOfWorkAttribute(implementationType) || AnyMethodHasUnitOfWorkAttribute(implementationType)) { return true; } // 第二種方式,即判斷具體類型是否繼承自 IUnitOfWorkEnabled 接口。 if (typeof(IUnitOfWorkEnabled).GetTypeInfo().IsAssignableFrom(implementationType)) { return true; } return false; }
在 ABP vNext 當中,將一些 職責 從原有的工做單元進行了 分離。抽象出了 IDatabaseApi
、ISupportsRollback
、ITransactionApi
這三個接口,這三個接口分別提供了不一樣的功能和職責。微服務
這裏以 IDatabaseApi
爲例,它是提供了一個 數據庫提供者(Database Provider) 的抽象概念,在 ABP vNext 裏面,是將 EFCore 做爲數據庫概念來進行抽象的。(由於後續 MongoDb 與 MemoryDb 與其同級)源碼分析
你能夠看做是 EF Core 的 Provider ,在 EF Core 裏面咱們能夠實現不一樣的 Provider ,來讓 EF Core 支持訪問不一樣的數據庫。
而 ABP vNext 這麼作的意圖就是提供一個統一的數據庫訪問 API,如何理解呢?這裏以 EFCoreDatabaseApi<TDbContext>
爲例,你查看它的實現會發現它繼承並實現了 ISupportsSavingChanges
,也就是說 EFCoreDatabaseApi<TDbContext>
支持 SaveChanges
操做來持久化數據更新與修改。
public class EfCoreDatabaseApi<TDbContext> : IDatabaseApi, ISupportsSavingChanges where TDbContext : IEfCoreDbContext { public TDbContext DbContext { get; } public EfCoreDatabaseApi(TDbContext dbContext) { DbContext = dbContext; } public Task SaveChangesAsync(CancellationToken cancellationToken = default) { return DbContext.SaveChangesAsync(cancellationToken); } public void SaveChanges() { DbContext.SaveChanges(); } }
也就是說 SaveChanges 這個操做,是 EFCore 這個 DatabaseApi 提供了一種特殊操做,是該類型數據庫的一種特殊接口。
若是針對於某些特殊的數據庫,例如 InfluxDb 等有一些特殊的 Api 操做時,就能夠經過一個 DatabaseApi 類型進行處理。
經過最開始的項目結構會發現一個 ITransactionApi
接口,這個接口只定義了一個 事務提交操做(Commit),並提供了異步方法的定義。
public interface ITransactionApi : IDisposable { void Commit(); Task CommitAsync(); }
跳轉到其典型實現 EfCoreTransactionApi
當中,能夠看到該類型還實現了 ISupportsRollback
接口。經過這個接口的名字,咱們大概就知道它的做用,就是提供了回滾方法的定義。若是某個數據庫支持回滾操做,那麼就能夠爲其實現該接口。
其實這裏按照語義,你也能夠將它放在 EfCoreDatabaseApi<TDbContext>
進行實現,由於回滾也是數據庫提供的 API 之一,只是在 ABP vNext 裏面又將其歸爲事務接口進行處理了。
這裏就再也不詳細贅述該類型的具體實現,後續會在單獨的 EF Core 章節進行說明。
在 ABP vNext 框架當中的工做單元實現,與原來 ABP 框架有一些不同。
首先說內部工做單元的定義,如今是有一個新的 ChildUnitOfWork
類型做爲 子工做單元。子工做單元自己並不會產生實際的業務邏輯操做,基本全部邏輯都是調用 UnitOfWork
的方法。
internal class ChildUnitOfWork : IUnitOfWork { public Guid Id => _parent.Id; public IUnitOfWorkOptions Options => _parent.Options; public IUnitOfWork Outer => _parent.Outer; public bool IsReserved => _parent.IsReserved; public bool IsDisposed => _parent.IsDisposed; public bool IsCompleted => _parent.IsCompleted; public string ReservationName => _parent.ReservationName; public event EventHandler<UnitOfWorkFailedEventArgs> Failed; public event EventHandler<UnitOfWorkEventArgs> Disposed; public IServiceProvider ServiceProvider => _parent.ServiceProvider; private readonly IUnitOfWork _parent; // 只有一個帶參數的構造函數,傳入的就是外部的工做單元(帶事務)。 public ChildUnitOfWork([NotNull] IUnitOfWork parent) { Check.NotNull(parent, nameof(parent)); _parent = parent; _parent.Failed += (sender, args) => { Failed.InvokeSafely(sender, args); }; _parent.Disposed += (sender, args) => { Disposed.InvokeSafely(sender, args); }; } // 下面全部 IUnitOfWork 的接口方法,都是調用傳入的 UnitOfWork 實例。 public void SetOuter(IUnitOfWork outer) { _parent.SetOuter(outer); } public void Initialize(UnitOfWorkOptions options) { _parent.Initialize(options); } public void Reserve(string reservationName) { _parent.Reserve(reservationName); } public void SaveChanges() { _parent.SaveChanges(); } public Task SaveChangesAsync(CancellationToken cancellationToken = default) { return _parent.SaveChangesAsync(cancellationToken); } public void Complete() { } public Task CompleteAsync(CancellationToken cancellationToken = default) { return Task.CompletedTask; } public void Rollback() { _parent.Rollback(); } public Task RollbackAsync(CancellationToken cancellationToken = default) { return _parent.RollbackAsync(cancellationToken); } public void OnCompleted(Func<Task> handler) { _parent.OnCompleted(handler); } public IDatabaseApi FindDatabaseApi(string key) { return _parent.FindDatabaseApi(key); } public void AddDatabaseApi(string key, IDatabaseApi api) { _parent.AddDatabaseApi(key, api); } public IDatabaseApi GetOrAddDatabaseApi(string key, Func<IDatabaseApi> factory) { return _parent.GetOrAddDatabaseApi(key, factory); } public ITransactionApi FindTransactionApi(string key) { return _parent.FindTransactionApi(key); } public void AddTransactionApi(string key, ITransactionApi api) { _parent.AddTransactionApi(key, api); } public ITransactionApi GetOrAddTransactionApi(string key, Func<ITransactionApi> factory) { return _parent.GetOrAddTransactionApi(key, factory); } public void Dispose() { } public override string ToString() { return $"[UnitOfWork {Id}]"; } }
雖然基本上全部方法的實現,都是調用的實際工做單元實例。可是有兩個方法 ChildUnitOfWork
是空實現的,那就是 Complete()
和 Dispose()
方法。
這兩個方法一旦在內部工做單元調用了,就會致使 事務被提早提交,因此這裏是兩個空實現。
下面就是上述邏輯的僞代碼:
using(var transactioinUow = uowMgr.Begin()) { // 業務邏輯 1 。 using(var childUow1 = uowMgr.Begin()) { // 業務邏輯 2。 using(var childUow2 = uowMgr.Begin()) { // 業務邏輯 3。 childUow2.Complete(); } childUow1.Complete(); } transactioinUow.Complete(); }
以上結構一旦某個內部工做單元拋出了異常,到會致使最外層帶事務的工做單元沒法調用 Complete()
方法,也就可以保證咱們的 數據一致性。
首先咱們查看 UnitOfWork
類型和 IUnitOfWork
的定義和屬性,能夠得到如下信息。
每一個工做單元是瞬時對象,由於它繼承了 ITransientDependency
接口。
每一個工做單元都會有一個 Guid
做爲其惟一標識信息。
每一個工做單元擁有一個 IUnitOfWorkOptions
來講明它的配置信息。
這裏的配置信息主要指一個工做單元在執行時的 超時時間,是否包含一個事務,以及它的 事務隔離級別(若是是事務性的工做單元的話)。
每一個工做單元存儲了 IDatabaseApi
與 ITransactionApi
的集合,並提供了訪問/存儲接口。
提供了兩個操做事件 Failed
與 Disposed
。
這兩個事件分別在工做單元執行失敗以及被釋放時(調用 Dispose()
方法)觸發,開發人員能夠掛載這兩個事件提供本身的處理邏輯。
工做單元還提供了一個工做單元完成事件組。
用於開發人員在工做單元完成時(調用Complete()
方法)掛載本身的處理事件,由於是 List<Func<Task>>
因此你能夠指定多個,它們都會在調用 Complete()
方法以後執行,例如以下代碼:
using (var uow = _unitOfWorkManager.Begin()) { uow.OnCompleted(async () => completed = true); uow.OnCompleted(async()=>Console.WriteLine("Hello ABP vNext")); uow.Complete(); }
以上信息是咱們查看了 UnitOfWork
的屬性與接口可以直接得出的結論,接下來我會根據一個工做單元的生命週期來講明一遍工做單元的實現。
一個工做單元的的構造是經過工做單元管理器實現的(IUnitOfWorkManager
),經過它的 Begin()
方法咱們會得到一個工做單元,至於這個工做單元是外部工做單元仍是內部工做單元,取決於開發人員傳入的參數。
public IUnitOfWork Begin(UnitOfWorkOptions options, bool requiresNew = false) { Check.NotNull(options, nameof(options)); // 得到當前的工做單元。 var currentUow = Current; // 若是當前工做單元不爲空,而且開發人員明確說明不須要構建新的工做單元時,建立內部工做單元。 if (currentUow != null && !requiresNew) { return new ChildUnitOfWork(currentUow); } // 調用 CreateNewUnitOfWork() 方法建立新的外部工做單元。 var unitOfWork = CreateNewUnitOfWork(); // 使用工做單元配置初始化外部工做單元。 unitOfWork.Initialize(options); return unitOfWork; }
這裏須要注意的就是建立新的外部工做單元方法,它這裏就使用了 IoC 容器提供的 Scope
生命週期,而且在建立以後會將最外部的工做單元設置爲最新建立的工做單元實例。
private IUnitOfWork CreateNewUnitOfWork() { var scope = _serviceProvider.CreateScope(); try { var outerUow = _ambientUnitOfWork.UnitOfWork; var unitOfWork = scope.ServiceProvider.GetRequiredService<IUnitOfWork>(); // 設置當前工做單元的外部工做單元。 unitOfWork.SetOuter(outerUow); // 設置最外層的工做單元。 _ambientUnitOfWork.SetUnitOfWork(unitOfWork); unitOfWork.Disposed += (sender, args) => { _ambientUnitOfWork.SetUnitOfWork(outerUow); scope.Dispose(); }; return unitOfWork; } catch { scope.Dispose(); throw; } }
上述描述可能會有些抽象,結合下面這兩幅圖可能會幫助你的理解。
咱們能夠在任何地方注入 IAmbientUnitOfWork
來獲取當前活動的工做單元,關於 IAmbientUnitOfWork
與 IUnitOfWorkAccessor
的默認實現,都是使用的 AmbientUnitOfWork
。
在該類型的內部,經過 AsyncLocal<IUnitOfWork>
來確保在不一樣的 異步上下文切換 過程當中,其值是正確且統一的。
構造了一個外部工做單元以後,咱們在倉儲等地方進行數據庫操做。操做完成以後,咱們須要調用 Complete()
方法來講明咱們的操做已經完成了。若是你沒有調用 Complete()
方法,那麼工做單元在被釋放的時候,就會產生異常,並觸發 Failed
事件。
public virtual void Dispose() { if (IsDisposed) { return; } IsDisposed = true; DisposeTransactions(); // 只有調用了 Complete()/CompleteAsync() 方法以後,IsCompleted 的值才爲 True。 if (!IsCompleted || _exception != null) { OnFailed(); } OnDisposed(); }
因此,咱們在手動使用工做單元管理器構造工做單元的時候,必定要注意調用 Complete()
方法。
既然 Complete()
方法這麼重要,它內部究竟作了什麼事情呢?下面咱們就來看一下。
public virtual void Complete() { // 是否已經進行了回滾操做,若是進行了回滾操做,則不提交工做單元。 if (_isRolledback) { return; } // 防止屢次調用 Complete 方法,原理就是看 _isCompleting 或者 IsCompleted 是否是已經爲 True 了。 PreventMultipleComplete(); try { _isCompleting = true; SaveChanges(); CommitTransactions(); IsCompleted = true; // 數據儲存了,事務提交了,則說明工做單元已經完成了,遍歷完成事件集合,依次調用這些方法。 OnCompleted(); } catch (Exception ex) { // 一旦在持久化或者是提交事務時出現了異常,則往上層拋出。 _exception = ex; throw; } } public virtual void SaveChanges() { // 遍歷集合,若是對象實現了 ISupportsSavingChanges 則調用相應的方法進行數據持久化。 foreach (var databaseApi in _databaseApis.Values) { (databaseApi as ISupportsSavingChanges)?.SaveChanges(); } } protected virtual void CommitTransactions() { // 遍歷事務 API 提供者,調用提交事務方法。 foreach (var transaction in _transactionApis.Values) { transaction.Commit(); } } protected virtual void RollbackAll() { // 回滾操做,仍是從集合裏面判斷是否實現了 ISupportsRollback 接口,來調用具體的實現進行回滾。 foreach (var databaseApi in _databaseApis.Values) { try { (databaseApi as ISupportsRollback)?.Rollback(); } catch { } } foreach (var transactionApi in _transactionApis.Values) { try { (transactionApi as ISupportsRollback)?.Rollback(); } catch { } } }
這裏能夠看到,ABP vNext 徹底剝離了具體事務或者回滾的實現方法,都是移動到具體的模塊進行實現的,也就是說在調用了 Complete()
方法以後,咱們的事務就會被提交了。
本小節從建立、提交、釋放這三個生命週期講解了工做單元的原理和實現,關於具體的事務和回滾實現,我會放在下一篇文章進行說明,這裏就再也不贅述了。
爲何工做單元經常配合 using 語句塊 使用,就是由於在提交工做單元以後,就能夠自動調用 Dispose()
方法,對工做單元的狀態進行校驗,而不須要咱們手動處理。
using(var uowA = _uowMgr.Begion()) { uowA.Complete(); }
在 ABP vNext 裏面,工做單元有了一個新的動做/屬性,叫作 是否保留(Is Reserved)。它的實現也比較簡單,指定了一個 ReservationName
,而後設置 IsReserved
爲 true
就完成了整個動做。
那麼它的做用是什麼呢?這塊內容我會在工做單元管理器小節進行解釋。
工做單元管理器在工做單元的原理/實現裏面已經有過了解,工做單元管理器主要負責工做單元的建立。
這裏我再挑選一個工做單元模塊的單元測試,來講明什麼叫作 保留工做單元。
[Fact] public async Task UnitOfWorkManager_Reservation_Test() { _unitOfWorkManager.Current.ShouldBeNull(); using (var uow1 = _unitOfWorkManager.Reserve("Reservation1")) { _unitOfWorkManager.Current.ShouldBeNull(); using (var uow2 = _unitOfWorkManager.Begin()) { // 此時 Current 值是 Uow2 的值。 _unitOfWorkManager.Current.ShouldNotBeNull(); _unitOfWorkManager.Current.Id.ShouldNotBe(uow1.Id); await uow2.CompleteAsync(); } // 這個時候,由於 uow1 是保留工做單元,因此不會被獲取到,應該爲 null。 _unitOfWorkManager.Current.ShouldBeNull(); // 調用了該方法,設置 uow1 的 IsReserved 屬性爲 false。 _unitOfWorkManager.BeginReserved("Reservation1"); // 得到到了值,而且誒它的 Id 是 uow1 的值。 _unitOfWorkManager.Current.ShouldNotBeNull(); _unitOfWorkManager.Current.Id.ShouldBe(uow1.Id); await uow1.CompleteAsync(); } _unitOfWorkManager.Current.ShouldBeNull(); }
經過對代碼的註釋和斷點調試的結果,咱們知道了經過 Reserved 建立的工做單元它的 IsReserved
屬性是 true
,因此咱們調用 IUnitOfWorkManager.Current
訪問的時候,會忽略掉保留工做單元,因此獲得的值就是 null
。
可是經過調用 BeginReserved(string name)
方法,咱們就能夠將指定的工做單元置爲 正常工做單元,這是由於調用了該方法以後,會從新調用工做單元的 Initialize()
方法,在該方法內部,又會將 IsReserved
設置爲 false
。
public virtual void Initialize(UnitOfWorkOptions options) { // ... 其餘代碼。 // 注意這裏。 IsReserved = false; }
保留工做單元的用途主要是在某些特殊場合,在某些特定條件下不想暴露給 IUnitOfWorkManager.Current
時使用。
若是咱們每一個地方都經過工做單元管理器來手動建立工做單元,那仍是比較麻煩的。ABP vNext 經過攔截器,來爲特定的類型(符合規則)自動建立工做單元。
關於攔截器的註冊已經在文章最開始說明了,這裏就再也不贅述,咱們直接來看攔截器的內部實現。其實在攔截器的內部,同樣是使用工做單元攔截器我來爲咱們建立工做單元的。只不過經過攔截器的方式,就可以無感知/無侵入地爲咱們構造健壯的數據持久化機制。
public override void Intercept(IAbpMethodInvocation invocation) { // 若是類型沒有標註 UnitOfWork 特性,或者沒有繼承 IUnitOfWorkEnabled 接口,則不建立工做單元。 if (!UnitOfWorkHelper.IsUnitOfWorkMethod(invocation.Method, out var unitOfWorkAttribute)) { invocation.Proceed(); return; } // 經過工做單元管理器構造工做單元。 using (var uow = _unitOfWorkManager.Begin(CreateOptions(invocation, unitOfWorkAttribute))) { invocation.Proceed(); uow.Complete(); } }
關於在 ASP.NET Core MVC 的工做單元過濾器,在實現上與攔截器大同小異,後續講解 ASP.NET Core Mvc 時再着重說明。
ABP vNext 框架經過統一工做單元爲咱們提供了健壯的數據庫訪問與持久化機制,使得開發人員在進行軟件開發時,只須要關注業務邏輯便可。不須要過多關注與數據庫等基礎設施的交互,這一切交由框架完成便可。
這裏多說一句,ABP vNext 自己就是面向 DDD 所設計的一套快速開發框架,包括值對象(ValueObject)這些領域驅動開發的特殊概念也被加入到框架實現當中。
微服務做爲 DDD 的一個典型實現,DDD 爲微服務的劃分提供理論支持。這裏爲你們推薦《領域驅動設計:軟件核心複雜性應對之道》這本書,該書籍由領域驅動設計的提出者編寫。
看了以後發如今大型系統當中(博主以前作 ERP 的,吃過這個虧)不少時候都是憑感受來寫,沒有一個具體的理論來支持軟件開發。最近拜讀了上述書籍以後,發現領域驅動設計(DDD)就是一套完整的方法論(固然 不是銀彈)。你們在學習並理解了領域驅動設計以後,使用 ABP vNext 框架進行大型系統開發就會更加駕輕就熟。
關於本系列文章的更新,由於最近本身在作 物聯網(Rust 語言學習、數字電路設計)相關的開發工做,因此 5 月到 6 月這段時間都沒怎麼去研究 ABP vNext。
最近在學習領域驅動設計的過程當中,發現 ABP vNext 就是爲 DDD 而生的,因此趁熱打鐵想將後續的 ABP vNext 文章一併更新,預計在 7 月內會把剩餘的文章補完(核心模塊)。