通用鏈接和事務管理方法
鏈接和事務管理是使用數據庫的應用程序最重要的概念之一。當你開啓一個數據庫鏈接,何時開始事務,如何釋放鏈接...諸如此類的。html
正如你們都知道的,.Net使用鏈接池(connection pooling)。所以,建立一個鏈接其實是從鏈接池中取得一個鏈接,會這麼作是由於建立新鏈接會有成本。若是沒有任何鏈接存在於鏈接池中,一個新的鏈接對象會被建立而且添加到鏈接池中。當你釋放鏈接,它其實是將這個鏈接對象送回到鏈接池。這並非實際意義上的釋放。這個機制是由.Net所提供的。所以,咱們應該在使用完以後釋放掉鏈接對象。這就是最佳實踐。數據庫
在應用程序中,有兩個通用的方來建立/釋放一個數據庫鏈接:安全
第一個方法:在Web請求到達的時候,建立一個鏈接對象。(Application_BeginRequest這個位於global.asax中的事件),使用同一個鏈接對象來處理全部的數據庫操做,而且在請求結束的時候關閉/釋放這個鏈接 (Application_EndRequest事件)。app
這是個簡易但卻沒效率的方法,緣由:框架
第二個方法: 建立一個鏈接當須要的時候(只要在使用它以前)而且釋放它在使用它以後。這是至關高效的,可是就得乏味並且反覆的去進行(建立/釋放鏈接)。ide
ABP的鏈接和事務管理
ABP綜合上述兩個鏈接管理的方法,而且提供一個簡單並且高效的模型。spa
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)的程序代碼。rest
假如倉儲方法調用另外一個倉儲方法(通常來講,若工做單元方法調用另外一個工做單元的方法),都使用同一個鏈接和事務。第一個被調用到的倉儲方法負責管理鏈接和事務,而其他被它調用的倉儲方法則只單純使用無論理。
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); }