ABP(現代ASP.NET樣板開發框架)系列之12、ABP領域層——工作單元(Unit Of work)
基於DDD的現代ASP.NET開發框架--ABP系列之12、ABP領域層——工作單元(Unit Of work)
ABP是“ASP.NET Boilerplate Project (ASP.NET樣板專案)”的簡稱。
通用連線和事務管理方法
連線和事務管理是使用資料庫的應用程式最重要的概念之一。當你開啟一個數據庫連線,什麼時候開始事務,如何釋放連線...諸如此類的。
正如大家都知道的,.Net使用連線池(connection pooling)。因此,建立一個連線實際上是從連線池中取得一個連線,會這麼做是因為建立新連線會有成本。如果沒有任何連線存在於連線池中,一個新的連線物件會被建立並且新增到連線池中。當你釋放連線,它實際上是將這個連線物件送回到連線池。這並不是實際意義上的釋放。這個機制是由.Net所提供的。因此,我們應該在使用完之後釋放掉連線物件。這就是最佳實踐。
在應用程式中,有兩個通用的方來建立/釋放一個數據庫連線:
第一個方法:在Web請求到達的時候,建立一個連線物件。(Application_BeginRequest這個位於global.asax中的事件),使用同一個連線物件來處理所有的資料庫操作,並且在請求結束的時候關閉/釋放這個連線 (Application_EndRequest事件)。
這是個簡易但卻沒效率的方法,原因:
- 或許這個Web請求不需要操作資料庫,但是連線卻會開啟。這對於連線池來說是個毫無效率的使用方式。
- 這可能會讓Web請求的執行時間變長,並且資料庫操作還會需要一些執行。這也是一種沒效率的連線池使用方式。
- 這對於Web應用來說是可行的。如果你的應用程式是Widnows Service,這可能就無法被實現了。
同樣的這是一個使用事務式的資料庫操作最佳場景。如果有一個操作發生失敗,所有的操作都會回滾。因為事務會鎖住資料庫中的一些資料列(事件資料表),它必定要是短暫的。
第二個方法: 建立一個連線當需要的時候(只要在使用它之前)並且釋放它在使用它之後。這是相當高效的,但是就得乏味而且反覆的去進行(建立/釋放連線)。
ABP的連線和事務管理
ABP綜合上述兩個連線管理的方法,並且提供一個簡單而且高效的模型。
倉儲類(Repository classes)
倉儲是主要的資料庫操作的類。ABP開啟了一個數據庫連線並且在進入到倉儲方法時會啟用一個事務。因此,你可以安全地使用連線於倉儲方法中。在倉儲方法結束後,事務會被提交併且會釋放掉連線。假如倉儲方法丟擲任何異常,事務會被回滾並且釋放掉連線。在這個模式中,倉儲方法是單元性的(一個工作單元unit of work)。ABP在處理上述那些動作都是全自動的。在這裡,有一個簡單的倉儲:
public class ContentRepository : NhRepositoryBase<Content>, IContentRepository { public List<Content> GetActiveContents(string searchCondition) { var query = from content in Session.Query<Content>() where content.IsActive && !content.IsDeleted select content; if (string.IsNullOrEmpty(searchCondition)) { query = query.Where(content => content.Text.Contains(searchCondition)); } return query.ToList(); } }
這個示例使用NHibernate作為ORM框架。如上所示,不需要撰寫任何資料庫連線操作(NHibernate中的Session)的程式程式碼。
假如倉儲方法呼叫另一個倉儲方法(一般來說,若工作單元方法呼叫另一個工作單元的方法),都使用同一個連線和事務。第一個被呼叫到的倉儲方法負責管理連線和事務,而其餘被它呼叫的倉儲方法則只單純使用不管理。
應用服務(Application service classes)
一個應用服務的方法也被考慮使用工作單元。如果我們擁有一個應用服務方法如下:
public class PersonAppService : IPersonAppService { private readonly IPersonRepository _personRepository; private readonly IStatisticsRepository _statisticsRepository; public PersonAppService(IPersonRepository personRepository, IStatisticsRepository statisticsRepository) { _personRepository = personRepository; _statisticsRepository = statisticsRepository; } public void CreatePerson(CreatePersonInput input) { var person = new Person { Name = input.Name, EmailAddress = input.EmailAddress }; _personRepository.Insert(person); _statisticsRepository.IncrementPeopleCount(); } }
在CreatePerson方法中,我們新增一個person使用person倉儲並且使用statistics倉儲增加總people數量。兩個倉儲共享同一個連線和事務於這個例子中,因為這是一個應用服務的方法。ABP開啟一個數據庫連線並且開啟一個事務於進入到CreationPerson這個方法,若沒有任何異常丟擲,接著提交這個事務於方法結尾時,若有異常被丟擲,則會回滾這個事務。在這種機制下,所有資料庫的操作在CreatePerson中,都成了單元性的了(工作單元)。
工作單元(Unit of work)
工作單元在後臺替倉儲和應用服務的方法工作。假如你想要控制資料庫的連線和事務,你就需要直接操作工作單元。下面有兩個直接使用的示例:
首要且最好的使用UnitOfWorkAttribute的方式如下:
[UnitOfWork] public void CreatePerson(CreatePersonInput input) { var person = new Person { Name = input.Name, EmailAddress = input.EmailAddress }; _personRepository.Insert(person); _statisticsRepository.IncrementPeopleCount(); }
因此,CreatePerson方法轉變成工作單元並且管理資料庫連線和事務,兩個倉儲物件都使用相同的工作單元。要注意,假如這是應用服務的方法則不需要新增UnitOfWork屬性,見工作單元方法:第三章,3.3.5。
第二個示例是使用IUnitOfWorkManager.Begin(...)方法如下所示:
public class MyService { private readonly IUnitOfWorkManager _unitOfWorkManager; private readonly IPersonRepository _personRepository; private readonly IStatisticsRepository _statisticsRepository; public MyService(IUnitOfWorkManager unitOfWorkManager, IPersonRepository personRepository, IStatisticsRepository statisticsRepository) { _unitOfWorkManager = unitOfWorkManager; _personRepository = personRepository; _statisticsRepository = statisticsRepository; } public void CreatePerson(CreatePersonInput input) { var person = new Person { Name = input.Name, EmailAddress = input.EmailAddress }; using (var unitOfWork = _unitOfWorkManager.Begin()) { _personRepository.Insert(person); _statisticsRepository.IncrementPeopleCount(); unitOfWork.Complete(); } } }
你可以注入並且使用IUnitOfWorkManager,如上所示。因此,你可以建立更多的有限範圍 (limited scope)的工作單元。在這個機制中,你通常可以手動呼叫Complete方法。如果你不呼叫,事務會回滾並且所有的異常都不會被儲存。Begin方法被重寫從而設定工作單元的選項。
這很棒,不過除非你有很好的理由,否則還是少用UnitOfWork屬性。
工作單元
禁用工作單元(Disabling unit of work)
你或許會想要禁用應用服務方法的工作單元(因為它預設是啟用的)。要想做到這個,使用UnitOfWorkAttribute的IsDisabled屬性。示例如下:
[UnitOfWork(IsDisabled = true)] public virtual void RemoveFriendship(RemoveFriendshipInput input) { _friendshipRepository.Delete(input.Id); }
平常時, 你不會需要這麼做,這是因為應用服務的方法都應該是單元性且通常是使用資料庫。在有些情況下,你或許會想要禁用應用服務的工作單元:
- 你的方法不需要任何資料庫操作且你不想要開啟那些不需要的資料庫連線
- 你想要使用工作單元於UnitOfWorkScope類的有限範圍內,如上所述
注意,如果工作單元方法呼叫這個RemoveFriendship方法,禁用被忽略且它和呼叫它的方法使用同一個工作單元。因此,使用禁用這個功能要很小心。同樣地,上述程式程式碼工作的很好,因為倉儲方法預設即為工作單元。
無事務的工作單元(Non-transactional unit of work)
工作單元預設上是具事務性的(這是它的天性)。因此,ABP啟動/提交/回滾一個顯性的資料庫等級的事務。在有些特殊案例中,事務可能會導致問題,因為它可能會鎖住有些資料列或是資料表於資料庫中。在此這些情境下, 你或許會想要禁用資料庫等級的事務。UnitOfWork屬性可以從它的建構子中取得一個布林值來讓它如非事務型工作單元般工作著。示例為:
[UnitOfWork(false)] public GetTasksOutput GetTasks(GetTasksInput input) { var tasks = _taskRepository.GetAllWithPeople(input.AssignedPersonId, input.State); return new GetTasksOutput { Tasks = Mapper.Map<List<TaskDto>>(tasks) }; }
建議可以這麼做[UnitOfWork(isTransaction:false)]。(具有可讀性並且明確)。
注意,ORM框架(像是NHibernate和EntityFramework)會在單一命令中於內部進行資料儲存。假設你更新了一些的實體於非事務的UoW。即便於這個情境下所有的更新都會於單一資料庫命令的工作單元尾部完成。但是,如果你直接執行SQL查詢,它會立即被執行。
這裡有一個非事務性UoW的限制。如果你已經位於事務性UoW區域內,設定isTransactional為false這個動作會被忽略。
使用非事務性UoW要小心,因為在大多數的情況下,資料整合應該是具事務性的。如果你的方法只是讀取資料,不改變資料,那麼當然可以採用非事務性。
工作單元呼叫其它工作單元(A unit of work method calls another)
若工作單元方法(一個貼上UnitOfWork屬性標籤的方法)呼叫另一個工作單元方法,他們共享同一個連線和事務。第一個方法管理連線,其它的方法只是使用它。這在所有方法都執行在同一個執行緒下是可行的(或是在同一個Web請求內)。實際上,當工作單元區域開始,所有的程式程式碼都會在同一個執行緒中執行並共享同一個連線事務,直到工作單元區域終止。這對於使用UnitOfWork屬性和UnitOfWorkScope類來說都是一樣的。如果你建立了一個不同的執行緒/任務,它使用自己所屬的工作單元。
自動化的saving changes (Automatically saving changes)
當我們使用工作單元到方法上,ABP自動的儲存所有變化於方法的末端。假設我們需要一個可更新person名稱的方法:
[UnitOfWork] public void UpdateName(UpdateNameInput input) { var person = _personRepository.Get(input.PersonId); person.Name = input.NewName; }
就這樣,名稱就被修改了!我們甚至沒有呼叫_personRepository.Update方法。ORM框架會持續追蹤實體所有的變化於工作單元內,且反映所有變化到資料庫中。
注意,這不需要在應用服務宣告UnitOfWork,因為它們預設就是採用工作單元。
倉儲介面的GetAll()方法(IRepository.GetAll())
當你在倉儲方法外呼叫GetAll方法, 這必定得有一個開啟狀態的資料庫連線,因為它返回IQueryable型別的物件。這是需要的,因為IQueryable延遲執行。它並不會馬上執行資料庫查詢,直到你呼叫ToList()方法或在foreach迴圈中使用IQueryable(或是存取被查詢結果集的情況下)。因此,當你呼叫ToList()方法,資料庫連線必需是啟用狀態。示例:
[UnitOfWork] public SearchPeopleOutput SearchPeople(SearchPeopleInput input) { //Get IQueryable<Person> var query = _personRepository.GetAll(); //Add some filters if selected if (!string.IsNullOrEmpty(input.SearchedName)) { query = query.Where(person => person.Name.StartsWith(input.SearchedName)); } if (input.IsActive.HasValue) { query = query.Where(person => person.IsActive == input.IsActive.Value); } //Get paged result list var people = query.Skip(input.SkipCount).Take(input.MaxResultCount).ToList(); return new SearchPeopleOutput { People = Mapper.Map<List<PersonDto>>(people) }; }
在這裡,SearchPeople方法必需是工作單元,因為IQueryable在被呼叫ToList()方法於方法本體內,並且資料庫連線必須於IQueryable.ToList()被執行時開啟。
一如GetAll()方法,如果需要資料庫連線且沒有倉儲的情況下,你就必須要使用工作單元。注意,應用服務方法預設就是工作單元。
工作單元屬性的限制(UnitOfWork attribute restrictions)
在下面情境下你可以使用UnitOfWork屬性標籤:
- 類所有public或public virtual這些基於介面的方法(像是應用服務是基於服務介面)
- 自我注入類的public virtual方法(像是MVC Controller和Web API Controller)
- 所有protected virtual方法。
建議將方法標示為virtual。你無法應用在private方法上。因為,ABP使用dynamic proxy來實現,而私有方法就無法使用繼承的方法來實現。當你不使用依賴注入且自行初始化類,那麼UnitOfWork屬性(以及任何代理)就無法正常運作。
選項
有許多可以用來控制工作單元的選項。
首先,我們可以在startup configuration中改變所有工作單元的所有預設值。這通常是用了我們模組中的PreInitialize方法來實現。
public class SimpleTaskSystemCoreModule : AbpModule { public override void PreInitialize() { Configuration.UnitOfWork.IsolationLevel = IsolationLevel.ReadCommitted; Configuration.UnitOfWork.Timeout = TimeSpan.FromMinutes(30); } //...other module methods }
方法
工作單元系統運作是無縫且不可視的。但是,在有些特例下,你需要呼叫它的方法。
SaveChanges
ABP儲存所有的變化於工作單元的尾端,你不需要做任何事情。但是,有些時候,你或許會想要在工作單元的過程中就儲存所有變化。在這個案例中,你可以注入IUnitOfWorkManager並且呼叫IUnitOfWorkManager.Current.SaveChanges()方法。示例中以Entity Framework在儲存變化時取得新增實體的Id。注意,當前工作單元是具事務性的,所有在事務中的變化會在異常發生時都被回滾,即便是已呼叫SaveChange。
事件
工作單元具有Completed/Failed/Disposed事件。你可以註冊這些事件並且進行所需的操作。注入IUnitOfWorkManager並且使用IUnitOfWorkManager.Current 屬性來取得當前已啟用的工作單元並且註冊它的事件。
你或許會想要執行有些程式程式碼於當前工作單元成功地完成。示例:
public void CreateTask(CreateTaskInput input) { var task = new Task { Description = input.Description }; if (input.AssignedPersonId.HasValue) { task.AssignedPersonId = input.AssignedPersonId.Value; _unitOfWorkManager.Current.Completed += (sender, args) => { /* TODO: Send email to assigned person */ }; } _taskRepository.Insert(task); }
希望更多國內的架構師能關注到ABP這個專案,也許這其中有能幫助到您的地方,也許有您的參與,這個專案可以發展得更好。
歡迎加ABP架構設計交流QQ群:134710707
相關推薦
ABP(現代ASP.NET樣板開發框架)系列之12、ABP領域層——工作單元(Unit Of work)
基於DDD的現代ASP.NET開發框架--ABP系列之12、ABP領域層——工作單元(Unit Of work) ABP是“ASP.NET Boilerplate Project (ASP.NET樣板專案)”的簡稱。 通用連線和事務管理方法 連線和事務管理是使用資料庫的應用程
ABP(現代ASP.NET樣板開發框架)系列之10、ABP領域層——實體
基於DDD的現代ASP.NET開發框架--ABP系列之10、ABP領域層——實體 ABP是“ASP.NET Boilerplate Project (ASP.NET樣板專案)”的簡稱。 本文由深圳-Carl提供翻譯 實體是DDD(領域驅動設計)的核心概念之一。Eric Eva
ABP(現代ASP.NET樣板開發框架)系列之4、ABP模組系統
基於DDD的現代ASP.NET開發框架--ABP系列之4、ABP模組系統 ABP是“ASP.NET Boilerplate Project (ASP.NET樣板專案)”的簡稱。 本文由東莞-天道提供翻譯 ABP模組系統簡介 ABP框架提供了建立和組裝模組的基礎,一個模組
ABP(現代ASP.NET樣板開發框架)系列之6、ABP依賴注入
基於DDD的現代ASP.NET開發框架--ABP系列之6、ABP依賴注入 ABP是“ASP.NET Boilerplate Project (ASP.NET樣板專案)”的簡稱。 本文由 上海-半冷 提供翻譯 什麼是依賴注入 如果你已經知道依賴注入的概念,建構函式和屬性注入
ABP(現代ASP.NET樣板開發框架)系列之9、ABP設定管理
基於DDD的現代ASP.NET開發框架--ABP系列之9、ABP設定管理 ABP是“ASP.NET Boilerplate Project (ASP.NET樣板專案)”的簡稱。 本文由山東-李偉提供翻譯 介紹 每個應用程式需要儲存一些設定並在應用程式的某個地方使用這些設定。
ABP(現代ASP.NET樣板開發框架)系列之2、ABP入門教程
基於DDD的現代ASP.NET開發框架--ABP系列之2、ABP入門教程 ABP是“ASP.NET Boilerplate Project (ASP.NET樣板專案)”的簡稱。 ASP.NET Boilerplate是一個用最佳實踐和流行技術開發現代WEB應用程式的新起點,它旨在成為一個通用的
ABP(現代ASP.NET樣板開發框架)系列之17、ABP應用層——引數有效性驗證
基於DDD的現代ASP.NET開發框架--ABP系列之17、ABP應用層——引數有效性驗證 ABP是“ASP.NET Boilerplate Project (ASP.NET樣板專案)”的簡稱。 應用程式的輸入資料首先應該被檢驗是否有效。輸入的資料能被使用者或其他應用程式提
ABP(現代ASP.NET樣板開發框架)系列之16、ABP應用層——資料傳輸物件(DTOs)
基於DDD的現代ASP.NET開發框架--ABP系列之16、ABP應用層——資料傳輸物件(DTOs) ABP是“ASP.NET Boilerplate Project (ASP.NET樣板專案)”的簡稱。 資料傳輸物件(Data Transfer Objects)用於應用層
ABP(現代ASP.NET樣板開發框架)系列之13、ABP領域層——資料過濾器(Data filters)
基於DDD的現代ASP.NET開發框架--ABP系列之13、ABP領域層——資料過濾器(Data filters) ABP是“ASP.NET Boilerplate Project (ASP.NET樣板專案)”的簡稱。 介紹 在資料庫開發中,我們一般會運用軟刪除(soft
ABP(現代ASP.NET樣板開發框架)系列之21、ABP展現層——Javascript函式庫
基於DDD的現代ASP.NET開發框架--ABP系列之21、ABP展現層——Javascript函式庫 ABP是“ASP.NET Boilerplate Project (ASP.NET樣板專案)”的簡稱。 ASP.NET Boilerplate的js庫提供了一些讓java
ABP(現代ASP.NET樣板開發框架)系列之14、ABP領域層——領域事件(Domain events)
基於DDD的現代ASP.NET開發框架--ABP系列之14、ABP領域層——領域事件(Domain events) ABP是“ASP.NET Boilerplate Project (ASP.NET樣板專案)”的簡稱。 在C#中,一個類可以定義其專屬的事件並且其它類可以註冊該事
ABP(現代ASP.NET樣板開發框架)系列之5、ABP啟動配置
基於DDD的現代ASP.NET開發框架--ABP系列之5、ABP啟動配置 ABP是“ASP.NET Boilerplate Project (ASP.NET樣板專案)”的簡稱。 本文由 東莞-天道 提供翻譯 譯者注:在看這一節的內容之前,建議大家先下載module-ze
ABP(現代ASP.NET樣板開發框架)系列之3、ABP分層架構
基於DDD的現代ASP.NET開發框架--ABP系列之3、ABP分層架構 ABP是“ASP.NET Boilerplate Project (ASP.NET樣板專案)”的簡稱。 前言 為了減少複雜性和提高程式碼的可重用性,採用分層架構是一種被廣泛接受的技術。為了實現分層的
ABP(現代ASP.NET樣板開發框架)系列之19、ABP應用層——審計日誌
基於DDD的現代ASP.NET開發框架--ABP系列之19、ABP應用層——審計日誌 ABP是“ASP.NET Boilerplate Project (ASP.NET樣板專案)”的簡稱。 維基百科定義:審計跟蹤(也稱為稽核日誌)是一個安全相關的時間順序記錄,記錄這些記錄的
ABP(現代ASP.NET樣板開發框架)系列之15、ABP應用層——應用服務(Application services)
基於DDD的現代ASP.NET開發框架--ABP系列之15、ABP應用層——應用服務(Application services) ABP是“ASP.NET Boilerplate Project (ASP.NET樣板專案)”的簡稱。 本文由東莞-天道提供翻譯 應用服務用於將領
ABP(現代ASP.NET樣板開發框架)系列之23、ABP展現層——異常處理
基於DDD的現代ASP.NET開發框架--ABP系列之23、ABP展現層——異常處理 ABP是“ASP.NET Boilerplate Project (ASP.NET樣板專案)”的簡稱。 在 web 應用程式中,異常通常是在 MVC Controller actions
ABP(現代ASP.NET樣板開發框架)系列之8、ABP日誌管理
基於DDD的現代ASP.NET開發框架--ABP系列之8、ABP日誌管理 ABP是“ASP.NET Boilerplate Project (ASP.NET樣板專案)”的簡稱。 本文由東莞-天道提供翻譯 Server side(伺服器端) ASP.NET Boilerpla
ABP(現代ASP.NET樣板開發框架)系列之1、ABP總體介紹
基於DDD的現代ASP.NET開發框架--ABP系列之1、ABP總體介紹 ABP是“ASP.NET Boilerplate Project (ASP.NET樣板專案)”的簡稱。 ASP.NET Boilerplate是一個用最佳實踐和流行技術開發現代WEB應用程式的新起點,它旨在成為一個通用的
ABP(現代ASP.NET樣板開發框架)系列之22、ABP展現層——導航欄設定
基於DDD的現代ASP.NET開發框架--ABP系列之22、ABP展現層——導航欄設定 ABP是“ASP.NET Boilerplate Project (ASP.NET樣板專案)”的簡稱。 每一個WEB應用程式都有導航選單,Abp也為使用者提供了通用的建立和顯示選單方式。
ABP(現代ASP.NET樣板開發框架)系列之7、ABP Session管理
基於DDD的現代ASP.NET開發框架--ABP系列之7、ABP Session管理 ABP是“ASP.NET Boilerplate Project (ASP.NET樣板專案)”的簡稱。 簡介 如果一個應用程式需要登入,則它必須知道當前使用者執行了什麼操作。因此ASP.