DDD领域驱动设计:CQRS
1 前置阅读
在阅读本文章之前,你可以先阅读:
- DDD领域驱动设计是什么
- DDD领域驱动设计:实体、值对象、聚合根
- DDD领域驱动设计:仓储
- MediatR一个优秀的.NET中介者框架
2 什么是CQRS?
CQRS,即命令和查询职责分离,是一种分离数据读取与写入的体系结构模式。 基本思想是把系统划分为两个界限:
- 查询,不改变系统的状态,且没有副作用。
- 命令,更改系统状态。
我们通过Udi Dahan的《Clarified CQRS》文章中的图来介绍一下:
2.1 查询 (Query)
上图中,可以看到Query不是通过DB来查询,而是通过一个专门用于查询的Cache(或ReadDB),ReadDB中的表是专门针对UI优化过的,例如最新的产品列表,销量最好的产品列表等,基本属于用空间换时间。
2.2 命令 (Command)
上图中,Command类似于Application Service,Command中主要做的事情有两个:1、通过调用领域层,把相关业务数据写入到DB中。2、同时更新ReadDB。
2.3 领域事件 (Domain Event)
上图中,更新ReadDB有两种方式,一种是直接在Command中进行更新,还有一种监听领域事件,把相应更改的数据同步到ReadDB中。
3 如何实现CQRS?
我们在这里使用最简单的方法:只将查询与命令分离,且执行这两种操作时使用相同的数据库。
3.1 命令 (Command)
首先,命令类
命令是让系统执行更改系统状态的操作的请求。 命令具有命令性,且应仅处理一次。
由于命令具有命令性,所以通常采用命令语气使用谓词(如“create”或“update”)命名,命令可能包括聚合类型,例如 CreateTodoCommand 与事件不同,命令不是过去发生的事实,它只是一个请求,因此可以拒绝它。
命令可能源自 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;
}
//...
}
DDD领域驱动设计:CQRS的更多相关文章
- 基于事件驱动的DDD领域驱动设计框架分享(附源代码)
原文:基于事件驱动的DDD领域驱动设计框架分享(附源代码) 补充:现在再回过头来看这篇文章,感觉当初自己偏激了,呵呵.不过没有以前的我,怎么会有现在的我和现在的enode框架呢?发现自己进步了真好! ...
- DDD领域驱动设计落地实践(十分钟看完,半小时落地)
一.引子 不知今年吹了什么风,忽然DDD领域驱动设计进入大家视野.该思想源于2003年 Eric Evans编写的"Domain-Driven Design领域驱动设计"简称DDD ...
- 浅谈我对DDD领域驱动设计的理解
从遇到问题开始 当人们要做一个软件系统时,一般总是因为遇到了什么问题,然后希望通过一个软件系统来解决. 比如,我是一家企业,然后我觉得我现在线下销售自己的产品还不够,我希望能够在线上也能销售自己的产品 ...
- DDD 领域驱动设计-商品建模之路
最近在做电商业务中,有关商品业务改版的一些东西,后端的架构设计采用现在很流行的微服务,有关微服务的简单概念: 微服务是一种架构风格,一个大型复杂软件应用由一个或多个微服务组成.系统中的各个微服务可被独 ...
- DDD 领域驱动设计-谈谈 Repository、IUnitOfWork 和 IDbContext 的实践(3)
上一篇:<DDD 领域驱动设计-谈谈 Repository.IUnitOfWork 和 IDbContext 的实践(2)> 这篇文章主要是对 DDD.Sample 框架增加 Transa ...
- DDD 领域驱动设计-两个实体的碰撞火花
上一篇:<DDD 领域驱动设计-领域模型中的用户设计?> 开源地址:https://github.com/yuezhongxin/CNBlogs.Apply.Sample(代码已更新) 在 ...
- DDD 领域驱动设计-谈谈 Repository、IUnitOfWork 和 IDbContext 的实践(2)
上一篇:<DDD 领域驱动设计-谈谈 Repository.IUnitOfWork 和 IDbContext 的实践(1)> 阅读目录: 抽离 IRepository 并改造 Reposi ...
- DDD 领域驱动设计-领域模型中的用户设计
上一篇:<DDD 领域驱动设计-如何控制业务流程?> 开源地址:https://github.com/yuezhongxin/CNBlogs.Apply.Sample(代码已更新,并增加了 ...
- DDD 领域驱动设计-如何控制业务流程?
上一篇:<DDD 领域驱动设计-如何完善 Domain Model(领域模型)?> 开源地址:https://github.com/yuezhongxin/CNBlogs.Apply.Sa ...
随机推荐
- 笔记本使用网线连接可以进行ftp下载,但是通过wifi连接只能登陆不能下载的问题。
环境: (1)服务器为阿里云服务器,有公网ip,有内网ip,公网和内网已经做了相关端口的映射,ftp服务器为FileZilla,ftp服务器被动模式已开启,防火墙已关闭 (2)ftp客户端为java写 ...
- 2.1 关系型数据的收集--Sqoop
Sqoop应用场景: 1.数据迁移,将关系型数据库中的数据导入Hadoop存储系统 2.可视化分析结果,将Hadoop处理之后产生的结果导入关系型数据库,以便进行可视化展示 3.数据增量导入:减少ha ...
- [强制]IDE使用UTF-8和LF
Java 程序开发常用的集成开发环境(IDE)有Eclipse和IntelliJ IDEA.Eclipse是免费开源的,很多早期程序员使用它的比较多.IntelliJ IDEA有社区版和企业版,社区版 ...
- 设计模式之单例模式(Singleton Pattern)深入浅出
单例模式介绍:单例模式是指确保一个类在任何情况下都绝对只有一个实例,并且提供一个全局的访问点.隐藏其所有构造方法,属于创新型模式. 常见的单例有:ServletContext.ServletConfi ...
- SAAS云平台搭建札记: (三) AntDesign + .Net Core WebAPI权限控制、动态菜单的生成
我们知道,当下最火的前端框架,非蚂蚁金服的AntDesign莫属,这个框架不仅在国内非常有名,在国外GitHub上React前端框架也排名第一.而且这个框架涵盖了React.Vue.Angular等多 ...
- laravel邮件发送
laravel邮件发送 使用邮件发送类Mail 文本 静态方法 raw() 富文本 静态方法 send() 注:使用邮件发送必须有邮件账号,需要开启smtp协议,现在主流服务器都支持,smtp默认端口 ...
- ArrayList源码解析--值得深读
ArrayList源码解析 基于jdk1.8 ArrayList的定义 类注释 允许put null值,会自动扩容: size isEmpty.get.set.add等方法时间复杂度是O(1): 是非 ...
- C++中的extern“C”
首先引入extern"C"的官方解释 extern "C" is meant to be recognized by a C++ compiler and to ...
- P1967 货车运输(倍增LCA,生成树)
题目链接: https://www.luogu.org/problemnew/show/P1967 题目描述 A国有n座城市,编号从 1到n,城市之间有 m 条双向道路.每一条道路对车辆都有重量限制, ...
- C语言中左值和右值的区别(C语言学习笔记)
重要的内容要重复强调: C语言的术语Ivalue指用于识别或定位一个存储位置的标识符.( 注意:左值同时还必须是可改变的) 其实rvalue的发明完全是为了搭配lvalue , rvalue你可以理解 ...