ABP是“ASP.NET Boilerplate Project (ASP.NET樣板項目)”的簡稱。
ASP.NET Boilerplate是一個用最佳實踐和流行技術開發現代WEB應用程序的新起點,它旨在成為一個通用的WEB應用程序框架和項目模板。
ABP的官方網站:http://www.aspnetboilerplate.com
ABP官方文檔:http://www.aspnetboilerplate.com/Pages/Documents
Github上的開源項目:https://github.com/aspnetboilerplate
一、公共連接和事務管理方法
在使用了數據庫的應用中,連接和事務管理是最重要的概念之一。何時打開一個連接,何時開始一個事務,如何釋放連接等等。
你可能已經知道,Net使用了連接池。因此,創建一個連接實際上是從連接池中獲取一個連接,因為因為創建一個連接是有消耗的。如果在連接池中沒有可用的連接,那么會創建一個新的連接,並將該連接加入連接池。當你釋放連接時,實際上是將該連接發送回給連接池,並沒有完全釋放。這種機制是.Net提供的立即可用的功能。因此,在我們使用完一個連接后應該立即釋放,在需要的時候才創建一個新的連接。總之,最佳實踐記住這八個字足矣:盡晚打開,盡早釋放。
在一個應用中創建或者釋放一個數據庫連接,通常有2種方法。
第一種方法:當Web請求開始(在Global.asax的Application_BeginRequest事件中)的時候創建一個連接,在所有的數據庫操作時使用相同的連接,並且在請求結束(Application_EndRequest)時關閉或者釋放該連接。這種方法很簡單但是不夠高效。why?
- 在一個請求中也許沒有數據庫操作,但是連接已經打開了。這造成了連接池的無效使用。
- 在一次請求中,可能請求需要消耗很長的時間而數據庫操作只花費很短的時間,這也會造成連接池的無效使用
- 這只在Web應用中是可行的。如果應用是一個Windows服務,那么可能不會實現。
以事務的方式執行數據庫操作已被認為是一種最佳實踐。如果一個操作失敗了,那么所有的操作都會回滾。因為一個事務可以鎖定數據庫中的一些行(甚至表),所以它必須是短暫存活的。
第二種方法:當需要時(僅在使用前)創建一個連接,使用后立即關閉。這是最有效的,但是到處創建或者釋放連接是一項重復乏味的工作。
二、ABP中的連接和事務管理
ABP兼備了這兩種方法並且提供了一個簡單而又有效的模型。
1、倉儲類
倉儲式執行數據庫操作主要的類。當進入一個倉儲方法時,ABP會打開一個數據庫連接(可能不是立即打開,但是在第一次使用數據庫時肯定是打開的,取決於ORM提供者的實現)並開始一個事務。因此,在一個倉儲方法中可以安全地使用連接。在方法的結束,事務被提交並且連接被釋放。如果倉儲方法拋出任何異常,那么事務都會回滾且連接被釋放。這樣一來,倉儲方法就是原子的(一個工作單元)。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、應用服務
一個應用服務也被認為是一個工作單元。假設我們有一個像下面的應用服務:
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倉儲增加總人數。在這里例子中,這兩個倉儲共享相同的連接和事務,因為它們在一個應用服務方法中。ABP在進入CreatePerson方法時打開一個數據庫連接並開始一個事務,如果沒有拋出異常事務會在方法結尾時提交,如果有任何異常發生,將會回滾。這樣一來,在CreatePerson方法中的所有數據庫操作都成了原子的(工作單元)。
3、工作單元
工作單元對於倉儲和應用服務方法隱式有效。如果你想在其他地方控制數據庫連接和事務,那么可以顯式使用它。
UnitOfWork特性:
最受人歡迎的方法是使用UnitOfWorkAttribute。例如:
[UnitOfWork] public void CreatePerson(CreatePersonInput input) { var person = new Person { Name = input.Name, EmailAddress = input.EmailAddress }; _personRepository.Insert(person); _statisticsRepository.IncrementPeopleCount(); }
這樣,CreatePerson方法變成了工作單元並且管理數據庫連接和事務,兩個倉儲使用相同的工作單元,注意的是,如果這是一個應用服務方法,就不需要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(); } } }
你可以注入然后使用IUnitOfWork,正如這里演示的這樣(如果你的應用繼承自ApplicationService類,那么你可以直接使用CurrentUnitOfWork屬性。如果沒有,你要先注入IUnitOfWorkManager)。這樣,你就可以創建更多的限制作用域的工作單元。用這種方法,你應該手動調用Complete方法。如果沒有調用,事務就會回滾,改變就不會保存。
Begin方法有很多重載來設置工作單元選項。
如果找不到一個很好的理由,建議還是使用UnitOfWork特性,因為代碼越短越好。
三、工作單元詳解
1、關閉工作單元
有時候你可能想關閉應用服務方法的工作單元(因為默認是開啟的),此時,可以使用UnitOfWorkAttribute的IsDisabled屬性。用法如下:
[UnitOfWork(IsDisabled = true)] public virtual void RemoveFriendship(RemoveFriendshipInput input) { _friendshipRepository.Delete(input.Id); }
正常情況下,不需要關閉數據單元,因為應用服務方法應該是原子的且一般都會使用數據庫。但也有些例外情況讓你想要關閉應用服務方法的工作單元:
- 方法不執行任何數據庫操作而且你也不想打開一個沒有必要的數據庫連接。
- 如上面描述的,你想要在一個UnitOfWorkScope類的有限作用域內使用工作單元。
注意:如果一個工作單元方法調用了這個RemoveFriendship方法,那么后者的關閉工作單元的功能將會失效,並且也會使用和調用者方法相同的工作單元。因此,要小心使用工作單元的關閉功能。
2、非事務的工作單元
工作單元默認是事務的(本質如此)。因此,ABP會開始->提交->回滾一個顯式的數據庫級別的事務。在一些特殊場合,事務可能會造成問題,因為它可能會鎖住數據庫中的一些行或者表。在這種情況下,你可能想關閉數據庫級別的事務。UnitOfWork特性可以在構造函數中獲得一個布爾值,從而以非事務形式工作。用法如下:
[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框架(如EF和NH)內部使用了一條單一命令來保存更改。假設你以非事務的UOW(工作單元)更新了一些實體的情景,甚至在這種情況下所有的更新都是在工作單元結束時以一個單一的數據庫命令執行的。但是如果你直接執行一個SQL查詢,它會立即執行。
非事務的UOW有一個限制。如果你已經處於一個事務的工作單元的作用域內,那么將isTransactional設置為false將會被忽略。
使用非事務的工作單元要小心,因為大多數時候對於數據的集成是事務的。如果你的方法只是讀數據,不需要改變數據,當然該方法是可以為非事務的了。
3、工作單元方法調用其它
如果一個工作單元的方法(使用了UnitOfWork特性聲明的方法)調用另一個工作單元的方法,那么它們共享相同的連接和事務。第一個方法管理連接,其他方法使用連接。這個對於運行在相同線程的方法是成立的(對於web應用則是相同的請求)。實際上,當一個工作單元作用域開始時,在同一線程執行的所有代碼都共享同一個連接和事務,直到工作單元作用域結束。這對於UnitOfWork特性和UnitOfWorkScope類都是成立的。
4、工作單元作用域
在其他事務中可以創建一個不同而又隔離的事務,或者可以在一個事務中創建一個非事務的作用域。.Net中定義了TransactionScopeOption,你可以為工作單元設置作用域選項。
5、自動保存
當我們為一個方法使用了工作單元時,ABP會在該方法結束時自動保存所有的更改。假設我們有一個更新person的name的方法:
[UnitOfWork] public void UpdateName(UpdateNameInput input) { var person = _personRepository.Get(input.PersonId); person.Name = input.NewName; }
你要做的就這么多,person的name就改變了。我們甚至不用調用_personRepository.Update方法。ORM框架會跟蹤工作單元中實體的所有改變,並將改變反應給數據庫。
注意沒有必要為應用服務方法聲明UnitOfWork特性,因為它們默認已經是工作單元了。
6、IRepository.GetAll()方法
當在一個倉儲方法之外調用GetAll()時,必須存在一個打開的數據庫連接,因為GetAll返回了IQueryable,而且IQueryable會延遲執行。直到調用ToList()方法或者在foreach循環中使用IQueryable,才會真正執行數據庫查詢。因此,調用ToList()方法時,數據庫連接必須是活着的(alive)。
思考下面的例子:
[UnitOfWork] public SearchPeopleOutput SearchPeople(SearchPeopleInput input) { //返回IQueryable<Person> var query = _personRepository.GetAll(); //添加一些過濾 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); } //獲得分頁結果列表 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()方法一樣,如果在倉儲之外需要數據庫連接,那么必須使用工作單元。應用服務方法默認是工作單元。
7、UnitOfWork特性的限制
UnitOfWork可以用於以下幾個條件:
- 所有用於接口的類的public或public virtual方法(如用於用於服務接口的應用服務類的方法)。
- 自注入類的所有public virtual(如MVC 控制器和Web Api控制器)。
- 所有的protected virtual方法。
建議總是將方法聲明為virtual,但是不能用於private方法。因為ABP為virtual方法私有了動態代理,private方法不能被派生的類訪問到。如果你沒有使用依賴注入且實例化類,那么UnitOfWork特性(和任何代理)就不能工作。
四、選項
有很多可以用於改變工作單元行為的選項。
首先,我們可以在啟動配置中更改所有工作單元的默認值。這通常是在模塊的PreInitialize方法中處理的。
public class SimpleTaskSystemCoreModule : AbpModule { public override void PreInitialize() { Configuration.UnitOfWork.IsolationLevel = IsolationLevel.ReadCommitted; Configuration.UnitOfWork.Timeout = TimeSpan.FromMinutes(30); } //...其他模塊方法 }
其次,我們可以為一個特定的工作單元重寫默認值。比如,UnitOfWork特性的構造函數和IUnitOfWorkManager的Begin方法都有獲得選項的重載。
五、方法
UnitOfWork系統無縫而不可見地工作。但是在某些場合,你需要調用它的方法。
SaveChanges:
ABP會在工作單元結束時保存所有更改,我們根本不用做任何事情。但是有時候你可能想在工作單元操作的中間將更改保存到數據庫中。在這種情況下,你可以注入IUnitOfWorkManager,然后調用IUnitOfWorkManager.Current.SaveChanges()方法。注意:如果當前的工作單元是事務的,那么如果有異常發生了,事務中的所有改變都會回滾,即使是已保存的改變。
六、事件
工作單元有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: 給派發的人發送郵件*/ }; } _taskRepository.Insert(task); }