文檔目錄
本節內容:
簡介
在一個使用數據庫的應用裡,連接和事務是非常重要的,何時打開一個連接,何時開啟一個事務,如果釋放連接等等。ABP通過工作單元系統管理數據庫的連接和事務。
在ABP中管理連接和事務
當進行一個工作單元方法,ABP打開一個數據庫連接(可能不是馬上打開,在第一次用到數據庫時打開,這要看ORM供應器的實現了)和開始一個事務,所以你可以安全地在這個方法內使用連接,在這個方法的最後,提交事務並釋放連接,如果這個方法拋出任何的異常,回滾事務並釋放連接,這種方式,一個工作單元就是原子性的。ABP自動完成這些操作。
如果一個工作單元方法調用另一個工作單元方法,兩個會使用同一個連接和事務,第一個方法管理連接和事務,另一個使用它們。
約定的工作單元方法
有些方法默認為工作單元方法:
假設我們有一個應用服務方法,如下所示:
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方法裡,我們使用人員倉儲插入一個人員,並使用統計倉儲遞增人員總數,這兩個倉儲共享相同的連接和事務,因為應用服務方法默認為一個工作單元。當進入CreatePerson方法時,ABP打開一個連接並開始一個事務,在方法結束時,如果沒有異常出現,就提交事務並釋放連接,這種方式,所有數據庫操作,在CreatePerson方法裡都變成是原子性的。
控制工作單元
上面的方法裡,工作單元是暗中工作的。web應用裡大部分情況,我們都不用控制工作單元。如果你想在某些地方控制工作單元,你可以顯式地使用它,有兩種方法可以實現。
UnitOfWork 特性
優先推薦的方法是:使用UnitOfWork特性,例如:
[UnitOfWork] public void CreatePerson(CreatePersonInput input) { var person = new Person { Name = input.Name, EmailAddress = input.EmailAddress }; _personRepository.Insert(person); _statisticsRepository.IncrementPeopleCount(); }
因此,CreatePerson方法成為一個工作單元,管理數據庫連接和事務,兩個倉儲使用相同的工作單元,注意,如果在一個應用服務方法裡,不需要使用UnitOfWork特性。 Also see 'unit of work method restrictions' section.
UnitOfWork特性有些選項,參考下方的“工作單元詳情”。
IUnitOfWorkManager
第二種方法是:使用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(有些基類默認已經被注入UnitOfWorkManager:Mvc控制器、應用服務、領域服務等),因此,你可以創建更多有域限制的工作單元。用這種方法,你應該手動調用Complete方法,如果你不調用,事務會回滾,修改不會被保存。
Begin方法有幾個設置工作單元選項的重載,如果沒有好的理由,最好使用UnitOfWork特性。
工作單元詳情
禁用工作單元
你如果想為約定的工作單元方法禁用工作單元,使用UnitOfWork特性的IsDisabled屬性。例如:
[UnitOfWork(IsDisabled = true)] public virtual void RemoveFriendship(RemoveFriendshipInput input) { _friendshipRepository.Delete(input.Id); }
通常,你不會想這麼做,但在有些情況下你需要禁用工作單元:
注意:如果一個工作單元方法調用這個RemoveFriendship方法,禁用特性會被忽略,該方法使用與調用者方法相同的工作單元。所以小心使用禁用特性。同樣地,上面的代碼也會工作得很好,因為倉儲方法默認是個工作單元。
非事務性工作單元
一個工作單元默認就是事務性的,因此ABP開始/提交/回滾數據庫級別事務。在一些特殊情況下,事務可能會引起問題,因為它可能鎖住數據庫的一些行或表。這種情況下,你可能想禁用數據庫級別事務。UnitOfWork特性可以在它的構造器裡,獲得一個boolean值作為非事務性標識。使用示例:
[UnitOfWork(isTransactional: false)] public GetTasksOutput GetTasks(GetTasksInput input) { var tasks = _taskRepository.GetAllWithPeople(input.AssignedPersonId, input.State); return new GetTasksOutput { Tasks = Mapper.Map<List<TaskDto>>(tasks) }; }
我建議[UnitOfWork(isTransactional:false)]這樣使用這個特性,我認為這樣更可讀更明確。但是你也可以[UnitOfWork(false)]這樣用。
注意:ORM框架(如NHibernate和EntityFramework)內部使用一個單獨的命令保存修改。假設你在一個非事務工作單元裡更新一些實體,即使是這種況下,所有更新也是在工作單元的最後,執行一個單獨的數據庫命令。但是,如果在一個非事務性工作單元裡,直接執行一條Sql語句,它會被直接執行並且不會被回滾。
非事務性工作單元有一個限制,如果你已經在一個事務性工作單元域內,把isTransactional設置false也會被忽略(在一個事務性工作單元裡,使用事務域選項創建一個非事務性工作單元)。
使用非事務性工作單元要小心,因為大部分情況下,應當用事務性保證數據完整性。如果你只是讀取數據,而不做修改,那麼就可以安全地使用非事務性。
一個工作單元方法調用另一個方法
工作單元是環繞的,如果一個工作單元方法調用另一個工作單元方法,他們共享相同的連接和事務,第一個方法管理連接和事務,另一個方法使用它們。
工作單元域
你可以在一個事務裡創建另一個獨立的事務,或在一個事務裡創建一個非事務性的域,.NET為它定義了TransactionScopeOption。你可以通過設置工作單元域選項來控制它。
自動保存修改
如果一個方法是工作單元,ABP會自動在這個方法的最後,保存所有修改。假設我們需要一個方法更新person的name:
[UnitOfWork] public void UpdateName(UpdateNameInput input) { var person = _personRepository.Get(input.PersonId); person.Name = input.NewName; }
這就是所有代碼,name會被修改!我們甚至不用調用_personRepository.Update方法。在一個工作單元裡,ORM框架跟蹤實體的所有修改,並反射到數據庫。
注意:不需要為約定的工作單元方法聲明UnitOfWork。
IRepository.GetAll() 方法
當你在一個倉儲方法外調用GetAll(),它必須要有一個打開的數據庫連接,因為它只是返回IQueryable。這是必須的,因為IQuery的延遲執行。它沒有執行數據庫查詢,除非你調用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()被執行時,數據庫連接必須打開著。
大多數情況,在一個web應用裡,使用GetAll方法是安全的,因為所以控制的Action默認都是工作單元並且數據庫連接在整個請求裡都是可用的。
UnitOfWrok 特性限制
可以使用UnitOfWork為:
建議一直使用virtual方法,你可以不為private方法這麼做,因為ABP為它們使用動態代理,而private方法對於繼承它的類來說是看不見的。如果不使用依賴注入並自己實例化這個類,UnitOfWork特性(和任何代理)都無法工作。
選項
有些選項可以改變一個工作單元的行為。
首先,我們可以在啟動配置裡修改所有工作單元的默認值,這通常在我們模塊的預初始化方法裡。
public class SimpleTaskSystemCoreModule : AbpModule { public override void PreInitialize() { Configuration.UnitOfWork.IsolationLevel = IsolationLevel.ReadCommitted; Configuration.UnitOfWork.Timeout = TimeSpan.FromMinutes(30); } //...other module methods }
其次,我們可以為一個特殊工作單元重寫默認值,為此,UnitOfWork特性構造器和IUnitOfWorkManager.Begin方法有獲取選項的重載。
最後,你可以通過啟動配置,為Asp.net Mvc、Web Api和Asp.net Core Mvc控制器配置工作單元特性默認值(查看它們各自的文檔)。
方法
UnitOfWork系統悄無聲息、無縫地工作著,但是在有些特殊情況下,你需要調用它們的方法。
你可以用如下作一種方式,訪問當前工作單元:
SaveChanges
ABP在一個工作單元的最後,保存所有修改,你不必做任何事,但是有時你想在一個工作單元操作的中間保存修改到數據庫,例如在EntityFramework裡,為獲取新實體的Id,先進行保存。
你可以使用當前工作單元的SaveChanges或SaveChangesAsync方法。
注意:如果當前工作單元是事務性的,所有的修改在出現異常情況下回滾,不然保存。
事件
一個工作單元有Completed、Failed和Disposed事件,你可以注冊這些事件然後執行需要的操作。例如,你想在當前工作單元成功完成後運行一些代碼:
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); }