一個業務功能往往不只由一次資料庫請求(或者服務調用)實現。為了功能的完整性,我們希望如果該功能執行一半時出錯,則撤銷前面已執行的改動。在資料庫層面上,事務管理實現了這種完整性需求。在ABP中,一個完整的業務功能稱為一個工作單元(Unit of Work,簡稱UoW)。工作單元代表一種完整的、原子性的 ...
一個業務功能往往不只由一次資料庫請求(或者服務調用)實現。為了功能的完整性,我們希望如果該功能執行一半時出錯,則撤銷前面已執行的改動。在資料庫層面上,事務管理實現了這種完整性需求。在ABP中,一個完整的業務功能稱為一個工作單元(Unit of Work,簡稱UoW)。工作單元代表一種完整的、原子性的操作。即一個工作單元包含的步驟要麼全部被執行,要麼都不被執行。如果執行一半時出現異常,則必須講已執行的步驟還原。通常我們將事務管理實現在工作單元中。下麵我們從ABP源碼入手研究如何使用工作單元。
ABP工作單元(UoW)的工作原理
ABP預設將工作單元應用在Repositories、 Application Services、MVC控制器和Web API控制器等組件。也就是說,這些組件的每個方法都是一個工作單元。ABP文檔對工作單元的原理講得不是很詳細,所以我們只能通過源碼進行研究。這裡我們以MVC控制器為例來瞭解一下ABP工作單元大致的工作原理。源碼分析比較枯燥,最好配套ABP源碼閱讀,或者跳到後面看粗體字結論。
ABP在Web模塊初始化時註冊了過濾器AbpMvcUowFilter
。AbpMvcUowFilter
在請求處理前(OnActionExecuting
方法)調用UnitOfWorkManager.Begin
方法來開始一個工作單元。UnitOfWorkManager.Begin
創建一個IUnitOfWork
的實例並賦值給ICurrentUnitOfWorkProvider.Current
,然後調用IUnitOfWork.Begin
方法開始一個工作單元。在請求處理結束後(OnActionExecuted
方法)如果處理過程沒有異常就調用IUnitOfWork.Complete
方法完成工作單元,並且無論請求處理是否成功,都調用IUnitOfWork.Dispose
來結束工作單元。
ABP提供了一個實現IUnitOfWork
的抽象基類UnitOfWorkBase
,另外還有個繼承了UnitOfWorkBase
的類NullUnitOfWork
。NullUnitOfWork
定義上面有一段註釋如此寫到:
/// <summary>
/// Null implementation of unit of work.
/// It's used if no component registered for <see cref="IUnitOfWork"/>.
/// This ensures working ABP without a database.
/// </summary>
public sealed class NullUnitOfWork : UnitOfWorkBase
NullUnitOfWork
是一個“空”的工作單元,它不會做任何操作。如果我們沒有在IoC容器中註冊其它IUnitOfWork的實現類,則ABP預設使用不做任何事的NullUnitOfWork
作為工作單元。所以如果我們要做一些保證功能完整性的工作(比如開啟資料庫事務),就要實現IUnitOfWork
並註冊到IoC容器。
閱讀UnitOfWorkBase
可以看到,UnitOfWorkBase
分別在Begin
方法、Complete
方法和Dispose
方法中調用了BeginUow
方法、CompleteUow
方法和DisposeUow
方法。我們需要重寫的主要是BeginUow
、CompleteUow
和DisposeUow
這三個方法。
通過源碼簡單瞭解了原理後,我們後面寫代碼要註意的有下麵幾點:
- 寫一個繼承
UnitOfWorkBase
的類UnitOfWork
,並實現介面ITransientDependency
保證UnitOfWork
被註冊到IoC容器; - 重寫方法
UnitOfWorkBase.BeginUow
,實現工作單元開始時的啟動操作; - 重寫方法
UnitOfWorkBase.CompleteUow
,實現工作單元正常結束時的保存操作; - 重寫方法
UnitOfWorkBase.DisposeUow
,實現工作單元結束時的清理操作; - 通過
ICurrentUnitOfWorkProvider.Current
來獲取當前的工作單元。
重寫SessionProvider,並實現工作單元
在之前文章(手工搭建基於ABP的框架(2) - 訪問資料庫)實現的LocalDbSessionProvider
中,為了追求代碼簡單,我們粗暴地用一個實質上是全局的變數來保存資料庫Session,在每次訪問資料庫時,flush上一個Session並創建新Session。另一方面,資料庫連接的配置、Session的創建保存、以及Session的提供都胡亂地放在了這個類里。這其實是非常不合理而且會引發很多問題的實現方法。
下麵我們重新設計這一塊邏輯。我們將LocalDbSessionProvider
所負責的功能拆分,分別實現在LocalDbSessionConfiguration
、UnitOfWork
和UnitOfWorkLocalDbSessionProvider
三個類中:
LocalDbSessionConfiguration
,單例。實現資料庫連接配置,提供資料庫Session工廠。public class LocalDbSessionConfiguration : ILocalDbSessionConfiguration, IDisposable { protected FluentConfiguration FluentConfiguration { get; private set; } public ISessionFactory SessionFactory { get; } public LocalDbSessionConfiguration() { FluentConfiguration = Fluently.Configure(); // 資料庫連接串 var connString = "data source=|DataDirectory|MySQLite.db;"; FluentConfiguration // 配置連接串 .Database(SQLiteConfiguration.Standard.ConnectionString(connString)) // 配置ORM .Mappings(m => m.FluentMappings.AddFromAssembly(Assembly.GetExecutingAssembly())); // 生成session factory SessionFactory = FluentConfiguration.BuildSessionFactory(); } public void Dispose() { SessionFactory.Dispose(); } }
UnitOfWork
,管理資料庫Session的生命期。資料庫Session和事務的創建、銷毀都封裝在這裡。這裡的一個問題是何時創建資料庫Session。一個自然的想法是在BeginUow
創建。然而這並不是一個很好的方式,會產生如下問題:1、預設情況下,Controllers、Application Services和Repositories都會開啟工作單元,也就是說,一次HTTP請求可能會多次開啟工作單元,導致過多地創建資料庫Session,甚至導致資料庫被鎖;2、即使某個介面不需要訪問資料庫,工作單元仍然會創建資料庫Session,浪費資源。正確的做法是在需要獲取資料庫Session的時候才進行創建。在下麵的實現中,我們將在UnitOfWork
實現一個GetOrCreateSession
方法來獲取資料庫Session。該方法在第一次調用時創建一個資料庫Session並開啟事務,後續調用則返回前面已創建的資料庫Session。後面UnitOfWorkLocalDbSessionProvider
將調用這個方法來獲取資料庫Session。public class UnitOfWork : UnitOfWorkBase, ITransientDependency { public ILocalDbSessionConfiguration DbSessionConfiguration { get; } private ISession _session; public UnitOfWork( IConnectionStringResolver connectionStringResolver, IUnitOfWorkDefaultOptions defaultOptions, IUnitOfWorkFilterExecuter filterExecuter, ILocalDbSessionConfiguration localDbSessionConfiguration) : base(connectionStringResolver, defaultOptions, filterExecuter) { DbSessionConfiguration = localDbSessionConfiguration; } public ISession GetOrCreateSession() { if (_session == null) { _session = DbSessionConfiguration.SessionFactory.OpenSession(); _session.BeginTransaction(); } return _session; } public override void SaveChanges() { _session?.Flush(); } public override Task SaveChangesAsync() { // 我們不用非同步Action,就不實現這個方法了。 throw new NotImplementedException(); } protected override void CompleteUow() { SaveChanges(); _session?.Transaction?.Commit(); } protected override Task CompleteUowAsync() { // 我們不用非同步Action,就不實現這個方法了。 throw new NotImplementedException(); } protected override void DisposeUow() { _session?.Transaction?.Dispose(); _session?.Dispose(); } } internal static class UnitOfWorkExtensions { public static ISession GetSession(this IActiveUnitOfWork unitOfWork) { if (unitOfWork == null) { throw new ArgumentNullException(nameof(unitOfWork)); } if (!(unitOfWork is UnitOfWork)) { throw new ArgumentException("unitOfWork is not type of " + typeof(UnitOfWork).FullName, nameof(unitOfWork)); } return (unitOfWork as UnitOfWork).GetOrCreateSession(); } }
UnitOfWorkLocalDbSessionProvider
,單例。通過當前的工作單元來提供資料庫Session。public class UnitOfWorkLocalDbSessionProvider : ISessionProvider, ISingletonDependency { private readonly ICurrentUnitOfWorkProvider _unitOfWorkProvider; public UnitOfWorkLocalDbSessionProvider(ICurrentUnitOfWorkProvider currentUnitOfWorkProvider) { _unitOfWorkProvider = currentUnitOfWorkProvider; } public ISession Session => _unitOfWorkProvider.Current?.GetSession(); }
最後,TweetRepository
和TweetQueryService
的構造函數用到了舊的LocalDbSessionProvider
,這兩處也需要改一下:
public TweetRepository()
: base(IocManager.Instance.Resolve<UnitOfWorkLocalDbSessionProvider>())
{ }
public TweetQueryService()
: base(IocManager.Instance.Resolve<UnitOfWorkLocalDbSessionProvider>())
{ }
使用NHProfiler進行驗證
上面實現了工作單元並封裝了資料庫事務管理。我們需要有方法驗證資料庫訪問時確實開啟了事務。NHProfiler是一個能夠監視NHibernate生成的SQL語句的工具。我們將使用NHProfiler查看生成的SQL,確認實現了工作單元後確實開啟了事務管理。
NHProfiler由兩個部分組成:
- 一個嵌入到我們應用的DLL。這個DLL會在NHibernate訪問資料庫時往本地socket發送生成的SQL語句。
- 客戶端。這個客戶端通過socket接收上面所說的DLL發送的數據並展示。
接下來我們的程式需要做一些小改動。首先MyTweet.Web
項目需要引用NHProfiler包里的HibernatingRhinos.Profiler.Appender.dll
。或者從Nuget添加NHibernateProfiler.Appender
包。如果你從NuGet添加的,則需要確認NuGet包的版本和客戶端的版本一致。
添加引用後,我們還需要在入口函數Application_Start
加上HibernatingRhinos.Profiler.Appender.NHibernate.NHibernateProfiler.Initialize()
這句語句來開啟NHProfiler的監聽:
protected override void Application_Start(object sender, EventArgs e)
{
AreaRegistration.RegisterAllAreas();
FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
RouteConfig.RegisterRoutes(RouteTable.Routes);
BundleConfig.RegisterBundles(BundleTable.Bundles);
HibernatingRhinos.Profiler.Appender.NHibernate.NHibernateProfiler.Initialize();
IocManager.Instance.IocContainer.AddFacility<LoggingFacility>(
f => f.UseAbpLog4Net().WithConfig("log4net.config"));
base.Application_Start(sender, e);
}
現在我們啟動MyTweet.Web
應用,然後打開NHProfiler客戶端。這時NHProfiler已經在監聽NHibernate生成的SQL了。到我們的應用新建一條tweet,再回到NHProfiler客戶端,可以看到,新建tweet的操作確實被事務包裹起來了。
總結
在本文中,我們自己繼承UnitOfWorkBase
實現工作單元,使得整個框架更靈活,更容易擴展。在現有代碼上稍作修改,我們還可以支持多資料庫事務。