新年伊始,祝你們喜樂如意,愛和幸福「鼠」不盡!♫. ♪~♬.♩♫~github
在上一篇 《如何運用領域驅動設計 - 存儲庫》 的文章中,咱們講述了有關倉儲的概念和使用規範。倉儲爲聚合提供了持久化到本地的功能,可是在持久化的過程當中,有時一個聚合根中的各個領域對象會分散到不一樣的數據庫表裏面;又或者是一個用例操做須要操做多個倉儲;而這些操做都應該要麼同時成功,要麼同時失敗,所以就須要爲這一系列操做提供事務的支持,而事務管理就是由工做單元來提供的。在上一篇中,可能已經提到了工做單元,可是僅僅是一筆帶過,如今咱們就來詳細的探究該如何更好的來實現工做單元。(文章的代碼片斷都使用的是C#,案例項目也是基於 DotNet Core 平臺)。數據庫
在上一篇文章中,已經爲你們提供了一個Github的Demo。若是已經下載過該Demo的同窗,您如今直接進行Pull就能夠得到最新的版本了;若是尚未下載該Demo的同窗也能夠戳下方的跳轉連接獲取。c#
GitHub 地址,點擊直達喲設計模式
在這裏咱們能夠先來看一下,該項目的應用代碼是什麼樣子:api
[HttpPost] public ActionResult<string> Add() { //使用倉儲來處理聚合 _itineraryRepository.Add( new Itinerary( "奧特曼", "賽文奧特曼", "傑克奧特曼", "佐菲奧特曼", "泰羅奧特曼")); _itineraryRepository.Add( new Itinerary( "蓋亞奧特曼", "戴拿奧特曼", "阿古茹奧特曼", "迪迦奧特曼", "")); return "success"; } [HttpGet] public ActionResult<long> Get() { var count = _itineraryRepository.GetCount(); return count; }
這是在Aspnet Core的Controller中的代碼,也就是對外提供的Api。能夠看到咱們僅僅只是經過倉儲的調用就完成了全部的操做。(ps:原諒我該演示api沒有遵循restful風格( ̄▽ ̄)",還有就是那些奧特曼。。。)。restful
您可能會說,這裏沒有作操做,那確定是在 ItineraryRepository 裏面作了手腳。好吧,下面咱們來看看該倉儲的實現。app
public class ItineraryRepository : EFRepository<UowAppDbContext, Itinerary, Guid> { public void Add(Itinerary itinerary) => DbContext.Set<Itinerary>().Add(itinerary); }
是的,它也只有這麼一點點代碼。而做爲後期的業務擴展和維護,咱們只須要完善咱們的Itinerary聚合(爲它擴展行爲和增長實體或值對象)以及ItineraryRepository倉儲(爲它添加對外檢索意圖的方法)就能夠了。框架
這種作法的好處可能您很快就能發現:在咱們代碼中到處都是關於領域對象的操做,儘量的避免其它基礎構建或功能支持組件來干擾程序。除了代碼量的減小以外,它也讓可讀性有着明顯的提升,若是在此基礎上可以構建出明確而乾淨的聚合根,那麼您的程序將具有更高的可擴展性。ide
好吧,回到咱們今天的主題:工做單元。其實上面的代碼就是對倉儲中工做單元的巧妙運用,它其實在後面默默的支持着程序的正常運轉,這是在調用層面上咱們徹底感受不到它的存在而已。下面就爲您介紹它是怎麼工做和實現的。
按照國際管理呢,這一章節都是解讀有關原著《領域驅動設計:軟件核心複雜性應對之道》 中的解釋。可是!!!有關工做單元的概念在書裏並無被明確的說起到。因此爲了證實咱們確確實實是在前人的基礎理念上來實踐,而不是胡編亂造本身隨便弄了一個概念出來。我特意去找了另一本較爲權威的領域驅動設計教材:《領域驅動設計模式、原理與實踐》 。在該書中對工做單元的解釋以下:
事務管理主要與應用程序服務層有關。存儲庫只與使用聚合根的單一集合的管理有關,而業務用例可能會形成對多個類型聚合的更新。事務管理是由工做單元處理的。工做單元模式的做用是保持追蹤業務任務期間聚合的全部變化。一旦全部的變化都已發生,則以後工做單元會協調事務中持久化存儲的更新。若是在將變動提交到數據存儲的中途出現了問題,那麼要確保不損壞數據完整性的話,就要回滾全部的變動以確保數據保持有效的狀態。
其實上文的話真的很好理解(相對於原著而言( ̄y▽, ̄)╭ )。首先咱們能夠獲得的第一個結論:事務管理實際上是應用服務層乾的事。第二個結論:事務的協調管理都是由工做單元來負責的
因此,咱們千萬不能由於工做單元和倉儲有聯繫就將它放置在領域層裏面:事務的提供每每是由數據庫管理程序來提供的,而這一類組件咱們通常將它們放置在基礎構架層,而領域層能夠依賴於基礎構架層,因此千萬要注意,保持您的領域層足夠乾淨,不要讓其它的東西干擾它,也更不要將事務處理這類東西放到了您的領域層來。(這一點,您會在後期MiCake <米蛋糕> 的使用中看到詳細的案例)。
實現工做單元,就是要實現倉儲中的事務操做。您可能已經看到過有些實現Repository的框架,它的寫法是注入一個unitOfWork,而後從uow中提取一個倉儲,而後再用倉儲來完成聚合根的持久化操做。相似的代碼就像這樣:
var yourRepository = uow.GetRepository<yourRepository>(); yourRepository.Add(yourEntity); uow.Commit();
這樣作沒有一點點的問題,並且是對工做單元和倉儲模式的完美實現。uow工做單元中維持了一個事務,從該工做單元中建立的每個倉儲均可以得到該事務,倉儲完成了本身的操做以後,工做單元使用Commit方法告訴事務管理器,該事務完成。
夏目去參加了妖怪的聚會,一回到家,貓咪老師就發現了它沾染了妖怪的味道
當倉儲的操做沾染上了工做單元的事務,它也就受到了事務的管理
若是您喜歡這種實現模式,能夠參考 threenine的Threenine.Data項目。
其實在剛開始,爲 MiCake(米蛋糕) 選取工做單元實現方案的時候,我也打算採用這種方式。可是在思考了一天以後,我仍是放棄了。由於我發現這種模式在完成每一次倉儲操做的時候,必需要從工做單元中去獲取。在Aspnet Core中,不得不在Controller中注入工做單元對象,而後再從該對象裏面去獲取倉儲。這顯然削弱了依賴注入所爲咱們提供的依賴閱讀性(本來在構造函數中,我能看出我須要注入的是A倉儲,可是如今我看到的只有工做單元)。
其實最重要的一點就是:我太懶啦 o_o ....。 爲何每次都要去多寫一個uow.GetXXXXX()。每使用一個倉儲就要多寫一次獲取語句,我就不能好好的只使用倉儲嗎? 因此在這個想法的強烈刺激下,我選取了另外的實現方法。
接下來,就讓咱們來實現最開始演示代碼中的工做單元吧。哦,對了,忘記說了,不管是演示的Github Demo仍是本次的博文,咱們都選取了Entity Framework Core來做爲數據持久組件。因此有些小夥伴會說,那我使用Dapper或者原生的ADO怎麼辦? 其實思路都是同樣的,您也能夠在看了EFCore的版本後,本身寫出對應的工做單元版本。若是有機會的話,歡迎在Github的Demo上直接添加,就能夠提交供更多的同窗參考啦。
腦殼裏有了這些還比較模糊的交互對象以後,咱們能夠來想一下一個倉儲完成添加聚合根的操做是怎麼樣的:
雖然步驟好像有5步,但總結下來,就是將具備事務的對象放置到工做單元中,讓它去負責提交。對!就是這麼簡單,該方法與上面那種從工做單元中獲取倉儲的方法想法,它是往工做單元中提交。因此,咱們此時能夠構造出一個僞代碼出來,大體理解它的實現:
//一、使用工做單元管理器建立一個工做單元 using (var uow = unitOfWorkManager.Create()) { //二、構造事務特徵對象,開啓事務並註冊到工做單元 RegisteTransactonFeature(DbContext); //三、執行倉儲中的內容 DbContext.Set<Itinerary>().Add(itinerary) //四、工做單元保存提交 uow.SaveChanges(); //五、dispose }
至少到目前,咱們能夠抽象出上面的各個對象了。
您也能夠先本身嘗試着想想,每一個對象接口應該實現什麼功能(方法)。
//首先是事務特徵對象,它提供了事務的基本Commit和Rollback方法 public interface ITransactionFeature { public bool IsCommit { get; } public bool IsRollback { get; } void Commit(); Task CommitAsync(CancellationToken cancellationToken = default); void Rollback(); Task RollbackAsync(CancellationToken cancellationToken = default); } //而後是事務特徵容器,它具備增長刪除事務特徵對象的方法 public interface ITransactionFeatureContainer { void RegisteTranasctionFeature(string key, ITransactionFeature TransactionFeature); ITransactionFeature GetOrAddTransactionFeature(string key, ITransactionFeature TransactionFeature); ITransactionFeature GetTransactionFeature(string key); void RemoveTransaction(string key); } //接下來是工做單元,它實現了事務特徵容器,而且對外提供提交的方法 public interface IUnitOfWork : ITransactionFeatureContainer { Guid ID { get; } bool IsDisposed { get; } void SaveChanges(); Task SaveChangesAsync(CancellationToken cancellationToken = default); void Rollback(); Task RollbackAsync(CancellationToken cancellationToken = default); } //最後是工做單元管理器,它提供了建立工做單元的方法 public interface IUnitOfWorkManager : IUnitOfWokrProvider, IDisposable { IUnitOfWork Create(); }
在構建出接口以後,咱們就能夠寫出具體的實現類了。首先是實現工做單元(UnitOfWork)對象。(因爲具體代碼實現較多,講解部分只選取了核心部分,完整代碼能夠參考Github的項目)
public class UnitOfWork : IUnitOfWork { private readonly Dictionary<string, ITransactionFeature> _transactionFeatures; public UnitOfWork() { _transactionFeatures = new Dictionary<string, ITransactionFeature>(); } //往容器中添加事物特徵對象 public virtual ITransactionFeature GetOrAddTransactionFeature( [NotNull]string key, [NotNull] ITransactionFeature transcationFeature) { if (_transactionFeatures.ContainsKey(key)) return _transactionFeatures.GetValueOrDefault(key); _transactionFeatures.Add(key, transcationFeature); return transcationFeature; } //對外提供的保存方法,執行該方法時調用容器內全部事物特徵對象的Commit方法 public virtual void SaveChanges() { foreach (var transactionFeature in _transactionFeatures.Values) { transactionFeature.Commit(); } } }
接下來就是與ORM框架關聯最深的事務特徵對象的實現了,因爲咱們選取了EF,因此此處應該實現EF版本的事務特徵對象:
public class EFTransactionFeature : ITransactionFeature { private IDbContextTransaction _dbContextTransaction; private DbContext _dbContext; public EFTransactionFeature(DbContext dbContext) { _dbContext = dbContext; } //設置事務 public void SetTransaction(IDbContextTransaction dbContextTransaction) { _isOpenTransaction = true; _dbContextTransaction = dbContextTransaction; } public void Commit() { if (IsCommit) return; IsCommit = true; //EF 事務的提交 _dbContext.SaveChanges(); _dbContextTransaction?.Commit(); } }
創建好了這兩個對象以後,其實咱們只須要一個流轉過程就能夠實現工做單元了。這個流程就是將事務特徵對象添加到工做單元中,可是咱們應該在何時將它添加進去呢?看過初版Github代碼的小夥伴可能知道,在倉儲調用的時候就能夠完成該操做。當時在初版中,咱們的實現代碼是這樣的:
public class EFRepository { protected IUnitOfWorkManager UnitOfWorkManager { get; private set; } protected DbContext DbContext { get; private set; } public EFRepository(IUnitOfWorkManager unitOfWorkManager, DbContext dbContext) { UnitOfWorkManager = unitOfWorkManager; DbContext = dbContext; } public void Add(TAggregateRoot aggregateRoot) { RegistUnitOfWork(DbContext); DbContext.Set<TAggregateRoot>().Add(aggregateRoot); } private void RegistUnitOfWork(DbContext dbContext) { string key = $"EFTransactionFeature - {dbContext.ContextId.InstanceId.ToString()}"; unitOfWork.ResigtedTransactionFeature(key, new EFTransactionFeature(DbContext)); } }
在每一次進行倉儲操做的時候,都調用了一個RegistUnitOfWork的方法,來完成事務特徵對象和工做單元的流轉工做。可是很快您就能發現問題:EFRepository是咱們實現的一個基類,之後全部的倉儲操做都繼承該類來完成操做,那不是每擴展一個方法,我都要在該方法中寫一句註冊代碼?若是我忘記寫了怎麼辦。還有一點,該註冊過程並無開啓一個事務,那麼事務是怎麼來的呢?
那麼怎麼才能避免用戶每一次都要去顯示調用註冊呢,而是讓用戶在不知不覺中就完成了該操做。因此咱們得思考在每個方法中,用戶都必定會寫的代碼是什麼,而後在該代碼上下手。可能您已經想到了,DbContext!!!是的,每個方法裏,用戶都會去寫DbContext,因此咱們能夠在他獲取DbContext的時候就完成註冊操做。因此,優化後的代碼就是這樣的:
public class EFRepository { public virtual TDbContext DbContext { get => _dbContextFactory.CreateDbContext(); } public void Add(TAggregateRoot aggregateRoot) { DbContext.Set<TAggregateRoot>().Add(aggregateRoot); } }
而該_dbContextFactory的實現就更簡單了,他要完成的任務就是註冊到工做單元而且開啓事務。
internal class UowDbContextFactory<TDbContext> { private readonly IUnitOfWorkManager _uowManager; public UowDbContextFactory(IUnitOfWorkManager uowManager) { _uowManager = uowManager; } public TDbContext CreateDbContext() { AddDbTransactionFeatureToUow(currentUow, DbContext); return wantedDbContext; } private void AddDbTransactionFeatureToUow(IUnitOfWork uow, TDbContext dbContext) { string key = $"EFCore - {dbContext.ContextId.InstanceId.ToString()}"; var efFeature = uow.GetOrAddTransactionFeature(key, new EFTransactionFeature(dbContext)); if (IsFeatureNeedOpenTransaction(uow, efFeature)) { var dbcontextTransaction = dbContext.Database.BeginTransaction(); efFeature.SetTransaction(dbcontextTransaction); } } private bool IsFeatureNeedOpenTransaction(IUnitOfWork uow, EFTransactionFeature efFeature) { return !efFeature.IsOpenTransaction; } }
dbContext.Database.BeginTransaction是EF爲咱們提供的手動開啓事務的方法。若是您嘗試實現另外ORM版本的工做單元,想一下在該ORM中是怎麼開啓的事務。
此時,咱們就已經實現了工做單元的流轉了,那麼還有一個問題就是:咱們怎麼默認去實現一個工做單元,而不是每一次都須要手動去開啓並提交。
AspNet Core爲咱們提供了很好的攔截方法。第一種方法: 咱們能夠在中間件中完成,由於全部的請求都要穿過中間件,咱們能夠在方法到API以前就開啓事務,等API訪問結束後就提交事務。第二種方法: 經過IActionFilter等週期接口來完成。本案例選取了第一種實現方法,您也能夠根據您本身的愛好選取本身的實現方式。
到這裏咱們已經實現了像上面Demo版本的工做單元,可是該工做單元其實還有許多特性沒有實現:
不過若是您的項目僅僅使用了一種ORM框架而且只須要開啓一個工做單元,那麼能夠嘗試使用該實現。
在實現MiCake真正的工做單元中,我嘗試了不少方法來解決上面的問題。在後面的文章中,您也會看到MiCake真正的工做單元。
附上一個當時寫工做單元的手記( ̄︶ ̄)↗
原本這篇文章原本不打算寫在《如何運用領域驅動設計》這個系列的,可是後來糾結了一下,仍是歸入了該系列。因爲該篇文章是實現工做單元的,因此代碼量就比較大,但願不會給您形成閱讀上的困難。下一篇的文章,是一個談了好久的問題————持久化值對象,如今終因而時候該解決它了。在本次Demo中您看到的聚合根Itinerary全部的屬性都是string,很顯然這是不符合常理的,因此在下一次就要讓它成爲真正的領域對象。(ps:改爲真正的領域對象後,感受均可以單體DDD應用落地了呢。( ̄︶ ̄)↗醒醒!少年。)爲了您不錯過下一篇文章的內容,您也可也點擊博客園右上角的關注,這樣就能及時收到更新了喲。