基於DDD的.NET開發框架 - ABP工做單元(Unit of Work)

返回ABP系列html

ABP是「ASP.NET Boilerplate Project (ASP.NET樣板項目)」的簡稱。git

ASP.NET Boilerplate是一個用最佳實踐和流行技術開發現代WEB應用程序的新起點,它旨在成爲一個通用的WEB應用程序框架和項目模板。github

ABP的官方網站:http://www.aspnetboilerplate.comweb

ABP官方文檔:http://www.aspnetboilerplate.com/Pages/Documents數據庫

Github上的開源項目:https://github.com/aspnetboilerplate安全

1、公共鏈接和事務管理方法app

在使用了數據庫的應用中,鏈接和事務管理是最重要的概念之一。什麼時候打開一個鏈接,什麼時候開始一個事務,如何釋放鏈接等等。框架

你可能已經知道,Net使用了鏈接池。所以,建立一個鏈接其實是從鏈接池中獲取一個鏈接,由於由於建立一個鏈接是有消耗的。若是在鏈接池中沒有可用的鏈接,那麼會建立一個新的鏈接,並將該鏈接加入鏈接池。當你釋放鏈接時,其實是將該鏈接發送回給鏈接池,並無徹底釋放。這種機制是.Net提供的當即可用的功能。所以,在咱們使用完一個鏈接後應該當即釋放,在須要的時候才建立一個新的鏈接。總之,最佳實踐記住這八個字足矣:盡晚打開,儘早釋放ide

在一個應用中建立或者釋放一個數據庫鏈接,一般有2種方法。函數

第一種方法:當Web請求開始(在Global.asax的Application_BeginRequest事件中)的時候建立一個鏈接,在全部的數據庫操做時使用相同的鏈接,而且在請求結束(Application_EndRequest)時關閉或者釋放該鏈接。這種方法很簡單可是不夠高效。why?

  • 在一個請求中也許沒有數據庫操做,可是鏈接已經打開了。這形成了鏈接池的無效使用。
  • 在一次請求中,可能請求須要消耗很長的時間而數據庫操做只花費很短的時間,這也會形成鏈接池的無效使用
  • 這隻在Web應用中是可行的。若是應用是一個Windows服務,那麼可能不會實現。

以事務的方式執行數據庫操做已被認爲是一種最佳實踐。若是一個操做失敗了,那麼全部的操做都會回滾。由於一個事務能夠鎖定數據庫中的一些行(甚至表),因此它必須是短暫存活的。

第二種方法:當須要時(僅在使用前)建立一個鏈接,使用後當即關閉。這是最有效的,可是處處建立或者釋放鏈接是一項重複乏味的工做。

2、ABP中的鏈接和事務管理

ABP兼備了這兩種方法而且提供了一個簡單而又有效的模型。

一、倉儲類

倉儲式執行數據庫操做主要的類。當進入一個倉儲方法時,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)打開或者關閉的代碼。

若是一個倉儲方法調用了其餘的倉儲方法(通常而言,若是一個工做單元調用了其餘的工做單元方法),那麼它們共享相同的鏈接和事務。第一個進入的方法管理鏈接和事務,其餘方法使用相同的鏈接和事務。

二、應用服務

一個應用服務也被認爲是一個工做單元。假設咱們有一個像下面的應用服務:

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方法中的全部數據庫操做都成了原子的(工做單元)。

三、工做單元

工做單元對於倉儲和應用服務方法隱式有效。若是你想在其餘地方控制數據庫鏈接和事務,那麼能夠顯式使用它。

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特性,由於代碼越短越好。

3、工做單元詳解

一、關閉工做單元

有時候你可能想關閉應用服務方法的工做單元(由於默認是開啓的),此時,可使用UnitOfWorkAttribute的IsDisabled屬性。用法以下:

[UnitOfWork(IsDisabled = true)]
public virtual void RemoveFriendship(RemoveFriendshipInput input)
{
    _friendshipRepository.Delete(input.Id);
}

正常狀況下,不須要關閉數據單元,由於應用服務方法應該是原子的且通常都會使用數據庫。但也有些例外狀況讓你想要關閉應用服務方法的工做單元:

  • 方法不執行任何數據庫操做並且你也不想打開一個沒有必要的數據庫鏈接。
  • 如上面描述的,你想要在一個UnitOfWorkScope類的有限做用域內使用工做單元。

注意:若是一個工做單元方法調用了這個RemoveFriendship方法,那麼後者的關閉工做單元的功能將會失效,而且也會使用和調用者方法相同的工做單元。所以,要當心使用工做單元的關閉功能。

二、非事務的工做單元

工做單元默認是事務的(本質如此)。所以,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將會被忽略。

使用非事務的工做單元要當心,由於大多數時候對於數據的集成是事務的。若是你的方法只是讀數據,不須要改變數據,固然該方法是能夠爲非事務的了。

三、工做單元方法調用其它

若是一個工做單元的方法(使用了UnitOfWork特性聲明的方法)調用另外一個工做單元的方法,那麼它們共享相同的鏈接和事務。第一個方法管理鏈接,其餘方法使用鏈接。這個對於運行在相同線程的方法是成立的(對於web應用則是相同的請求)。實際上,當一個工做單元做用域開始時,在同一線程執行的全部代碼都共享同一個鏈接和事務,直到工做單元做用域結束。這對於UnitOfWork特性和UnitOfWorkScope類都是成立的。

四、工做單元做用域

在其餘事務中能夠建立一個不一樣而又隔離的事務,或者能夠在一個事務中建立一個非事務的做用域。.Net中定義了TransactionScopeOption,你能夠爲工做單元設置做用域選項。

五、自動保存

當咱們爲一個方法使用了工做單元時,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特性,由於它們默認已是工做單元了。

六、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()方法同樣,若是在倉儲以外須要數據庫鏈接,那麼必須使用工做單元。應用服務方法默認是工做單元。

七、UnitOfWork特性的限制

UnitOfWork能夠用於如下幾個條件:

  • 全部用於接口的類的public或public virtual方法(如用於用於服務接口的應用服務類的方法)。
  • 自注入類的全部public virtual(如MVC 控制器和Web Api控制器)。
  • 全部的protected virtual方法。

建議老是將方法聲明爲virtual,可是不能用於private方法。由於ABP爲virtual方法私有了動態代理,private方法不能被派生的類訪問到。若是你沒有使用依賴注入且實例化類,那麼UnitOfWork特性(和任何代理)就不能工做。

4、選項

有不少能夠用於改變工做單元行爲的選項。

首先,咱們能夠在啓動配置中更改全部工做單元的默認值。這一般是在模塊的PreInitialize方法中處理的。

public class SimpleTaskSystemCoreModule : AbpModule
{
    public override void PreInitialize()
    {
        Configuration.UnitOfWork.IsolationLevel = IsolationLevel.ReadCommitted;
        Configuration.UnitOfWork.Timeout = TimeSpan.FromMinutes(30);
    }

    //...其餘模塊方法
}

其次,咱們能夠爲一個特定的工做單元重寫默認值。好比,UnitOfWork特性的構造函數和IUnitOfWorkManager的Begin方法都有得到選項的重載。

5、方法

UnitOfWork系統無縫而不可見地工做。可是在某些場合,你須要調用它的方法。

SaveChanges:

ABP會在工做單元結束時保存全部更改,咱們根本不用作任何事情。可是有時候你可能想在工做單元操做的中間將更改保存到數據庫中。在這種狀況下,你能夠注入IUnitOfWorkManager,而後調用IUnitOfWorkManager.Current.SaveChanges()方法。注意:若是當前的工做單元是事務的,那麼若是有異常發生了,事務中的全部改變都會回滾,即便是已保存的改變。

6、事件

工做單元有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);
}
相關文章
相關標籤/搜索