一、簡要說明 統一工作單元是一個比較重要的基礎設施組件,它負責管理整個業務流程當中涉及到的資料庫事務,一旦某個環節出現異常自動進行回滾處理。 在 ABP vNext 框架當中,工作單元被獨立出來作為一個單獨的模塊( Volo.Abp.Uow )。你可以根據自己的需要,來決定是否使用統一工作單元。 二 ...
一、簡要說明
統一工作單元是一個比較重要的基礎設施組件,它負責管理整個業務流程當中涉及到的資料庫事務,一旦某個環節出現異常自動進行回滾處理。
在 ABP vNext 框架當中,工作單元被獨立出來作為一個單獨的模塊(Volo.Abp.Uow)。你可以根據自己的需要,來決定是否使用統一工作單元。
二、源碼分析
整個 Volo.Abp.Uow 項目的結構如下,從下圖還是可以看到我們的老朋友 IUnitOfWorkManager
和 IUnitOfWork
,不過也多了一些新東西。看一個模塊的功能,首先從它的 Module
入手,我們先看一下 AbpUnitofWorkModule
裡面的實現。
2.1 工作單元的初始模塊
打開 AbpUnitOfWorkModule
裡面的代碼,發現還是有點失望,裡面就一個服務註冊完成事件。
public override void PreConfigureServices(ServiceConfigurationContext context)
{
context.Services.OnRegistred(UnitOfWorkInterceptorRegistrar.RegisterIfNeeded);
}
這裡的結構和之前看的 審計日誌 模塊類似,就是註冊攔截器的作用,沒有其他特別的操作。
2.1.1 攔截器註冊
繼續跟進代碼,其實現是通過 UnitOfWorkHelper
來確定哪些類型應該集成 UnitOfWork
組件。
public static void RegisterIfNeeded(IOnServiceRegistredContext context)
{
// 根據回調傳入的 context 綁定的實現類型,判斷是否應該為該類型註冊 UnitOfWorkInterceptor 攔截器。
if (UnitOfWorkHelper.IsUnitOfWorkType(context.ImplementationType.GetTypeInfo()))
{
context.Interceptors.TryAdd<UnitOfWorkInterceptor>();
}
}
繼續分析 UnitOfWorkHelper
內部的代碼,第一種情況則是實現類型 (implementationType
) 或類型的任一方法標註了 UnitOfWork
特性的話,都會為其註冊工作單元攔截器。
第二種情況則是 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;
}
2.2 新的介面與抽象
在 ABP vNext 當中,將一些 職責 從原有的工作單元進行了 分離。抽象出了 IDatabaseApi
、ISupportsRollback
、ITransactionApi
這三個介面,這三個介面分別提供了不同的功能和職責。
2.2.1 資料庫統一訪問介面
這裡以 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 類型進行處理。
2.2.2 資料庫事務介面
通過最開始的項目結構會發現一個 ITransactionApi
介面,這個介面只定義了一個 事務提交操作(Commit),並提供了非同步方法的定義。
public interface ITransactionApi : IDisposable
{
void Commit();
Task CommitAsync();
}
跳轉到其典型實現 EfCoreTransactionApi
當中,可以看到該類型還實現了 ISupportsRollback
介面。通過這個介面的名字,我們大概就知道它的作用,就是提供了回滾方法的定義。如果某個資料庫支持回滾操作,那麼就可以為其實現該介面。
其實這裡按照語義,你也可以將它放在 EfCoreDatabaseApi<TDbContext>
進行實現,因為回滾也是資料庫提供的 API 之一,只是在 ABP vNext 裡面又將其歸為事務介面進行處理了。
這裡就不再詳細贅述該類型的具體實現,後續會在單獨的 EF Core 章節進行說明。
2.3 工作單元的原理與實現
在 ABP vNext 框架當中的工作單元實現,與原來 ABP 框架有一些不一樣。
2.3.1 內部工作單元 (子工作單元)
首先說內部工作單元的定義,現在是有一個新的 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()
方法,也就能夠保證我們的 數據一致性。
2.3.2 外部工作單元
首先我們查看 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();
}
2.3.3 保留工作單元
在 ABP vNext 裡面,工作單元有了一個新的動作/屬性,叫做 是否保留(Is Reserved)。它的實現也比較簡單,指定了一個 ReservationName
,然後設置 IsReserved
為 true
就完成了整個動作。
那麼它的作用是什麼呢?這塊內容我會在工作單元管理器小節進行解釋。
2.4 工作單元管理器
工作單元管理器在工作單元的原理/實現裡面已經有過瞭解,工作單元管理器主要負責工作單元的創建。
這裡我再挑選一個工作單元模塊的單元測試,來說明什麼叫做 保留工作單元。
[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
時使用。
2.5 工作單元攔截器
如果我們每個地方都通過工作單元管理器來手動創建工作單元,那還是比較麻煩的。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 月內會把剩餘的文章補完(核心模塊)。