通用連接和事務管理方法
連接和事務管理是使用數據庫的應用程序最重要的概念之一。當你開啟一個數據庫連接,什麼時候開始事務,如何釋放連接...諸如此類的。
正如大家都知道的,.Net使用連接池(connection pooling)。因此,創建一個連接實際上是從連接池中取得一個連接,會這麼做是因為創建新連接會有成本。如果沒有任何連接存在於連接池中,一個新的連接對象會被創建並且添加到連接池中。當你釋放連接,它實際上是將這個連接對象送回到連接池。這並不是實際意義上的釋放。這個機制是由.Net所提供的。因此,我們應該在使用完之後釋放掉連接對象。這就是最佳實踐。
在應用程序中,有兩個通用的方來創建/釋放一個數據庫連接:
第一個方法:在Web請求到達的時候,創建一個連接對象。(Application_BeginRequest這個位於global.asax中的事件),使用同一個連接對象來處理所有的數據庫操作,並且在請求結束的時候關閉/釋放這個連接 (Application_EndRequest事件)。
這是個簡易但卻沒效率的方法,原因:
第二個方法: 創建一個連接當需要的時候(只要在使用它之前)並且釋放它在使用它之後。這是相當高效的,但是就得乏味而且反復的去進行(創建/釋放連接)。
ABP的連接和事務管理
ABP綜合上述兩個連接管理的方法,並且提供一個簡單而且高效的模型。
1.倉儲類(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)的程序代碼。
假如倉儲方法調用另一個倉儲方法(一般來說,若工作單元方法調用另一個工作單元的方法),都使用同一個連接和事務。第一個被調用到的倉儲方法負責管理連接和事務,而其余被它調用的倉儲方法則只單純使用不管理。
2.應用服務(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中,都成了單元性的了(工作單元)。
3.工作單元(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屬性。
工作單元
1.禁用工作單元(Disabling unit of work)
你或許會想要禁用應用服務方法的工作單元(因為它默認是啟用的)。要想做到這個,使用UnitOfWorkAttribute的IsDisabled屬性。示例如下:
[UnitOfWork(IsDisabled = true)] public virtual void RemoveFriendship(RemoveFriendshipInput input) { _friendshipRepository.Delete(input.Id); }
平常時, 你不會需要這麼做,這是因為應用服務的方法都應該是單元性且通常是使用數據庫。在有些情況下,你或許會想要禁用應用服務的工作單元:
(1)你的方法不需要任何數據庫操作且你不想要開啟那些不需要的數據庫連接
(2)你想要使用工作單元於UnitOfWorkScope類的有限范圍內,如上所述
注意,如果工作單元方法調用這個RemoveFriendship方法,禁用被忽略且它和調用它的方法使用同一個工作單元。因此,使用禁用這個功能要很小心。同樣地,上述程序代碼工作的很好,因為倉儲方法默認即為工作單元。
2.無事務的工作單元(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要小心,因為在大多數的情況下,數據整合應該是具事務性的。如果你的方法只是讀取數據,不改變數據,那麼當然可以采用非事務性。
3.工作單元調用其它工作單元(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,因為它們默認就是采用工作單元。
4.倉儲接口的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()方法,如果需要數據庫連接且沒有倉儲的情況下,你就必須要使用工作單元。注意,應用服務方法默認就是工作單元。
5.工作單元屬性的限制(UnitOfWork attribute restrictions)
在下面情境下你可以使用UnitOfWork屬性標簽:
(1)類所有public或public virtual這些基於界面的方法(像是應用服務是基於服務界面)
(2)自我注入類的public virtual方法(像是MVC Controller和Web API Controller)
(3)所有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); }