DDD領域驅動設計:CQRS

1 前置閱讀

在閱讀本文章以前,你能夠先閱讀:數據庫

  • DDD領域驅動設計是什麼
  • DDD領域驅動設計:實體、值對象、聚合根
  • DDD領域驅動設計:倉儲
  • MediatR一個優秀的.NET中介者框架

2 什麼是CQRS?

CQRS,即命令和查詢職責分離,是一種分離數據讀取與寫入的體系結構模式。 基本思想是把系統劃分爲兩個界限:api

  • 查詢,不改變系統的狀態,且沒有反作用。
  • 命令,更改系統狀態。

咱們經過Udi Dahan的《Clarified CQRS》文章中的圖來介紹一下:app

2.1 查詢 (Query)

上圖中,能夠看到Query不是經過DB來查詢,而是經過一個專門用於查詢的Cache(或ReadDB),ReadDB中的表是專門針對UI優化過的,例如最新的產品列表,銷量最好的產品列表等,基本屬於用空間換時間。框架

2.2 命令 (Command)

上圖中,Command相似於Application Service,Command中主要作的事情有兩個:一、經過調用領域層,把相關業務數據寫入到DB中。二、同時更新ReadDB。dom

2.3 領域事件 (Domain Event)

上圖中,更新ReadDB有兩種方式,一種是直接在Command中進行更新,還有一種監聽領域事件,把相應更改的數據同步到ReadDB中。async

3 如何實現CQRS?

咱們在這裏使用最簡單的方法:只將查詢與命令分離,且執行這兩種操做時使用相同的數據庫。微服務

3.1 命令 (Command)

首先,命令類優化

命令是讓系統執行更改系統狀態的操做的請求。 命令具備命令性,且應僅處理一次。ui

因爲命令具備命令性,因此一般採用命令語氣使用謂詞(如「create」或「update」)命名,命令可能包括聚合類型,例如 CreateTodoCommand 與事件不一樣,命令不是過去發生的事實,它只是一個請求,所以能夠拒絕它。this

命令可能源自 UI,由用戶發出請求而產生,也可能來自進程管理器,由進程管理器指導聚合執行操做而產生。

命令的一個重要特徵是它應該由單一接收方處理,且僅處理一次。 這是由於命令是要在應用程序中執行的單個操做或事務。 例如,同一個「建立待辦事項」的處理次數不該超過一次。 這是命令和事件之間的一個重要區別。 事件可能會通過屢次處理,由於許多系統或微服務可能會對該事件感興趣。

命令經過包含數據字段或集合(其中包含執行命令所需的全部信息)的類實現。 命令是一種特殊的數據傳輸對象 (DTO),專門用於請求更改或事務。 命令自己徹底基於處理命令所需的信息,別無其餘。

下面的示例顯示了簡化的 CreateTodoCommand 類。

public class CreateTodoCommand : IRequest<TodoDTO>
{
    public Guid Id { get; set; }
    public string Name { get; set; }
}

而後,命令處理程序類

應爲每一個命令實現特定命令處理程序類。 這是該模式的工做原理,是應用命令對象、域對象和基礎結構存儲庫對象的情景。

命令處理程序收到命令,並從使用的聚合獲取結果。 結果應爲成功執行命令,或者異常。 出現異常時,系統狀態應保持不變。

命令處理程序一般執行如下步驟:

  • 它接收 DTO 等命令對象。
  • 它會驗證命令是否有效。
  • 它會實例化做爲當前命令目標的聚合根實例。
  • 它會在聚合根實例上執行方法,從命令得到所需數據。
  • 它將聚合的新狀態保持到相關數據庫。

一般狀況下,命令處理程序處理由聚合根(根實體)驅動的單個聚合。 若是多個聚合應受到單個命令接收的影響,可以使用域事件跨多個聚合傳播狀態或操做。

做爲命令處理程序類的示例,下面的代碼演示本章開頭介紹的同一個 CreateTodoCommandHandler 類。 這個示例還強調了 Handle 方法以及域模型對象/聚合的操做。

public class CreateTodoCommandHandler
        : IRequestHandler<CreateTodoCommand, TodoDTO>
{
    private readonly IRepository repository;
    private readonly IMapper mapper;

    public CreateTodoCommandHandler(IRepository repository, IMapper mapper)
    {
        this.repository = repository ?? throw new ArgumentNullException(nameof(repository));
        this.mapper = mapper ?? throw new ArgumentNullException(nameof(mapper));
    }

    public async Task<TodoDTO> Handle(CreateTodoCommand message, CancellationToken cancellationToken)
    {
        var todo = Todo.Create(message.Name);
        repository.Entry(todo);
        await repository.SaveAsync();

        var todoForDTO = mapper.Map<TodoDTO>(todo);
        return todoForDTO;
    }
}

最後,經過MediatR實現命令進程管道
首先,讓咱們看一下示例 WebAPI 控制器,你會在其中使用MediatR,如如下示例所示:

[Route("api/[controller]")]
[ApiController]
public class TodosController : ControllerBase
{
    //...
    private readonly MediatR.IMediator mediator;
    public TodosController(MediatR.IMediator mediator)
    {
        this.mediator = mediator ?? throw new ArgumentNullException(nameof(mediator));
    }
    //...
}

在控制器方法中,將命令發送到MediatR的代碼幾乎只有一行:

[HttpPost]
public async Task<ActionResult<TodoDTO>> Create(CreateTodoCommand param)
{
    var ret = await mediator.Send(param);
    return CreatedAtAction(nameof(Get), new { id = ret.Id }, ret);
}

3.2 查詢 (Query)

首先,定義DTO

[Table("T_Todo")]
public class TodoDTO
{
    #region Public Properties
    public Guid Id { get; set; }
    public string Name { get; set; }
    #endregion                
}

而後,建立具體的查詢方法

public class TodoQueries
{
    private readonly TodoingQueriesContext context;
    public TodoQueries(TodoingQueriesContext context)
    {
        this.context = context;
    }

    //...
    public async Task<PaginatedItems<TodoDTO>> Query(int pageIndex, int pageSize)
    {
        var total = await context.Todos
            .AsNoTracking()
            .CountAsync();

        var todos = await context.Todos
            .AsNoTracking()
            .OrderBy(o => o.Id)
            .Skip(pageSize * (pageIndex - 1))
            .Take(pageSize)
            .ToListAsync();

        return new PaginatedItems<TodoDTO>(total, todos);
    }
    //...
}

請注意TodoingQueriesContext和命令處理中的Context不是同一個,實現查詢端除了用EFCore、還能夠用存儲過程、視圖、具體化視圖或Dapper等等。

最後,調用查詢方法

[Route("api/[controller]")]
[ApiController]
public class TodosController : ControllerBase
{
    private readonly TodoQueries todoQueries;
    public TodosController(TodoQueries todoQueries)
    {
        this.todoQueries = todoQueries ?? throw new ArgumentNullException(nameof(todoQueries));
    }

    //...
    [HttpGet]
    public async Task<ActionResult<PaginatedItems<TodoDTO>>> Query(int pageIndex, int pageSize)
    {
        return todoQueries.Query(pageIndex, pageSize).Result;
    }
    //...
}
相關文章
相關標籤/搜索