介绍
CQRS(命令查询职责分离模式)从业务上分离修改 (Command,增,删,改,会对系统状态进行修改)和查询(Query,查,不会对系统状态进行修改)的行为。从而使得逻辑更加清晰,便于对不同部分进行针对性的优化。
CQRS基本思想在于,任何一个对象的方法可以分为两大类
- 命令(Command):不返回任何结果(void),但会改变对象的状态。
- 查询(Query):返回结果,但是不会改变对象的状态,对系统没有副作用。
本文主要介绍如何使用基于MediatR
实现的Abp.Cqrs
类库,以及如何从读写分离模式来思考问题.
本文旨在探索cqrs如果落地,目前仅支持单机模式,不支持分布式。
本文案例主要介绍了命令的使用方式,分离写的职责,对event没有过多的介绍和使用。
源码:
- https://github.com/ZhaoRd/abp_cqrs
- https://github.com/ZhaoRd/abp_cqrs_example
项目案例 -- 电话簿 (后端)
本案例后端使用abp官方模板,可在https://aspnetboilerplate.com/Templates 创建项目,前端使用的是 ng-alain模板。
引入 Abp.Cqrs 类库
在
core
项目中安装cqrs包,并且添加模块依赖
在命令或事件处理类的项目中,注册cqrs处理
添加电话簿实体类
电话簿实体类代码,代码封装了简单的业务修改联系方式
和修改姓名
,封装这两个业务,主要是为了命令服务,注意实体类的属性可访问性是protected
,这意味着该实体具有不可变性,如果要修改实体属性(修改对象状态),只能通过实体提供的业务方法进行修改。
///
/// 电话簿
///
public class TelephoneBook : FullAuditedAggregateRoot
{
///
/// 初始化 实例
///
public TelephoneBook()
{
}
///
/// 初始化 实例
///
public TelephoneBook([NotNull]string name, string emailAddress, string tel)
{
this.Name = name;
this.EmailAddress = emailAddress;
this.Tel = tel;
}
///
/// 姓名
///
public string Name { get; protected set; }
///
/// 邮箱
///
public string EmailAddress { get; protected set; }
///
/// 电话号码
///
public string Tel { get; protected set; }
///
/// 修改联系方式
///
///
///
public void Change(string emailAddress,string tel)
{
this.EmailAddress = emailAddress;
this.Tel = tel;
}
///
/// 修改姓名
///
///
public void ChangeName(string name)
{
this.Name = name;
}
}
更新ef脚本
在AddressBookDbContext
中添加一下代码
public DbSet TelephoneBooks { get; set; }
执行脚本add-migration Add_TelephoneBook
和update-database
定义 创建、更新、删除命令
///
/// 创建电话簿命令
///
public class CreateTelephoneBookCommand:Command
{
public TelephoneBookDto TelephoneBook { get;private set; }
public CreateTelephoneBookCommand(TelephoneBookDto book)
{
this.TelephoneBook = book;
}
}
///
/// 更新电话命令
///
public class UpdateTelephoneBookCommand : Command
{
public TelephoneBookDto TelephoneBook { get; private set; }
public UpdateTelephoneBookCommand(TelephoneBookDto book)
{
this.TelephoneBook = book;
}
}
///
/// 删除电话簿命令
///
public class DeleteTelephoneBookCommand : Command
{
public EntityDto TelephoneBookId { get; private set; }
public DeleteTelephoneBookCommand(EntityDto id)
{
this.TelephoneBookId = id;
}
}
命令代码很简单,只要提供命令需要的数据即可
命令处理类
cqrs中,是通过命令修改实体属性的,所以命令处理类需要依赖相关仓储。
关注更新命令处理,可以看到不是直接修改实体属性,而是通过实体提供的业务方法修改实体属性。
///
/// 更新电话簿命令处理
///
public class UpdateTelephoneBookCommandHandler : ICommandHandler
{
private readonly IRepository _telephoneBookRepository;
public UpdateTelephoneBookCommandHandler(IRepository telephoneBookRepository)
{
this._telephoneBookRepository = telephoneBookRepository;
}
public async Task Handle(UpdateTelephoneBookCommand request, CancellationToken cancellationToken)
{
var tenphoneBook = await this._telephoneBookRepository.GetAsync(request.TelephoneBook.Id.Value);
tenphoneBook.Change(request.TelephoneBook.EmailAddress,request.TelephoneBook.Tel);
return Unit.Value;
}
}
///
/// 删除电话簿命令
///
public class DeleteTelephoneBookCommandHandler : ICommandHandler
{
private readonly IRepository _telephoneBookRepository;
public DeleteTelephoneBookCommandHandler(
IRepository telephoneBookRepository)
{
this._telephoneBookRepository = telephoneBookRepository;
}
public async Task Handle(DeleteTelephoneBookCommand request, CancellationToken cancellationToken)
{
await this._telephoneBookRepository.DeleteAsync(request.TelephoneBookId.Id);
return Unit.Value;
}
}
///
/// 创建电话簿命令
///
public class CreateTelephoneBookCommandHandler : ICommandHandler
{
private readonly IRepository _telephoneBookRepository;
public CreateTelephoneBookCommandHandler(IRepository telephoneBookRepository)
{
this._telephoneBookRepository = telephoneBookRepository;
}
public async Task Handle(CreateTelephoneBookCommand request, CancellationToken cancellationToken)
{
var telephoneBook = new TelephoneBook(request.TelephoneBook.Name, request.TelephoneBook.EmailAddress, request.TelephoneBook.Tel);
await this._telephoneBookRepository.InsertAsync(telephoneBook);
return Unit.Value;
}
}
DTO类定义
DTO负责和前端交互数据
[AutoMap(typeof(TelephoneBook))]
public class TelephoneBookDto : EntityDto
{
///
/// 姓名
///
public string Name { get; set; }
///
/// 邮箱
///
public string EmailAddress { get; set; }
///
/// 电话号码
///
public string Tel { get; set; }
}
[AutoMap(typeof(TelephoneBook))]
public class TelephoneBookListDto : FullAuditedEntityDto
{
///
/// 姓名
///
public string Name { get; set; }
///
/// 邮箱
///
public string EmailAddress { get; set; }
///
/// 电话号码
///
public string Tel { get; set; }
}
实现应用层
应用层需要依赖两个内容
-
命令总线
负责发送命令 -
仓储
负责查询功能
在应用层中不在直接修改实体属性
观察创建
编辑
删除
业务,可以看到这些业务都在做意见事情:发布命令.
public class TelephoneBookAppServiceTests : AddressBookTestBase
{
private readonly ITelephoneBookAppService _service;
public TelephoneBookAppServiceTests()
{
_service = Resolve();
}
///
/// 获取所有看通讯录
///
///
[Fact]
public async Task GetAllTelephoneBookList_Test()
{
// Act
var output = await _service.GetAllTelephoneBookList();
// Assert
output.Count().ShouldBe(0);
}
///
/// 创建通讯录
///
///
[Fact]
public async Task CreateTelephoneBook_Test()
{
// Act
await _service.CreateOrUpdate(
new TelephoneBookDto()
{
EmailAddress = "[email protected]",
Name = "赵云",
Tel="12345678901"
});
await UsingDbContextAsync(async context =>
{
var zhaoyun = await context
.TelephoneBooks
.FirstOrDefaultAsync(u => u.Name == "赵云");
zhaoyun.ShouldNotBeNull();
});
}
///
/// 更新通讯录
///
///
[Fact]
public async Task UpdateTelephoneBook_Test()
{
// Act
await _service.CreateOrUpdate(
new TelephoneBookDto()
{
EmailAddress = "[email protected]",
Name = "赵云",
Tel = "12345678901"
});
var zhaoyunToUpdate = await UsingDbContextAsync(async context =>
{
var zhaoyun = await context
.TelephoneBooks
.FirstOrDefaultAsync(u => u.Name == "赵云");
return zhaoyun;
});
zhaoyunToUpdate.ShouldNotBeNull();
await _service.CreateOrUpdate(
new TelephoneBookDto()
{
Id = zhaoyunToUpdate.Id,
EmailAddress = "[email protected]",
Name = "赵云",
Tel = "12345678901"
});
await UsingDbContextAsync(async context =>
{
var zhaoyun = await context
.TelephoneBooks
.FirstOrDefaultAsync(u => u.Name == "赵云");
zhaoyun.ShouldNotBeNull();
zhaoyun.EmailAddress.ShouldBe("[email protected]");
});
}
///
/// 删除通讯录
///
///
[Fact]
public async Task DeleteTelephoneBook_Test()
{
// Act
await _service.CreateOrUpdate(
new TelephoneBookDto()
{
EmailAddress = "[email protected]",
Name = "赵云",
Tel = "12345678901"
});
var zhaoyunToDelete = await UsingDbContextAsync(async context =>
{
var zhaoyun = await context
.TelephoneBooks
.FirstOrDefaultAsync(u => u.Name == "赵云");
return zhaoyun;
});
zhaoyunToDelete.ShouldNotBeNull();
await _service.Delete(
new EntityDto()
{
Id = zhaoyunToDelete.Id
});
await UsingDbContextAsync(async context =>
{
var zhaoyun = await context
.TelephoneBooks
.Where(c=>c.IsDeleted == false)
.FirstOrDefaultAsync(u => u.Name == "赵云");
zhaoyun.ShouldBeNull();
});
}
}
项目案例 -- 电话簿 (前端 ng-alain 项目)
使用ng-alain实现前端项目
界面预览
列表界面代码
import { Component, OnInit, Injector } from '@angular/core';
import { _HttpClient, ModalHelper } from '@delon/theme';
import { SimpleTableColumn, SimpleTableComponent } from '@delon/abc';
import { SFSchema } from '@delon/form';
import { finalize } from 'rxjs/operators';
import { AppComponentBase } from '@shared/app-component-base';
import { TelephoneBookServiceProxy, TelephoneBookDto, TelephoneBookListDto } from '@shared/service-proxies/service-proxies';
import { BooksCreateComponent } from './../create/create.component'
import { BooksEditComponent } from './../edit/edit.component'
@Component({
selector: 'books-list',
templateUrl: './list.component.html',
})
export class BooksListComponent extends AppComponentBase implements OnInit {
params: any = {};
list = [];
loading = false;
constructor( injector: Injector,private http: _HttpClient, private modal: ModalHelper,
private _telephoneBookService:TelephoneBookServiceProxy) {
super(injector);
}
ngOnInit() {
this.loading = true;
this._telephoneBookService
.getAllTelephoneBookList()
.pipe(finalize(
()=>{
this.loading = false;
}
))
.subscribe(res=>{
this.list = res;
})
;
}
edit(id: string): void {
this.modal.static(BooksEditComponent, {
bookId: id
}).subscribe(res => {
this.ngOnInit();
});
}
add() {
this.modal
.static(BooksCreateComponent, { id: null })
.subscribe(() => this.ngOnInit());
}
delete(book: TelephoneBookListDto): void {
abp.message.confirm(
"删除通讯录 '" + book.name + "'?"
).then((result: boolean) => {
console.log(result);
if (result) {
this._telephoneBookService.delete(book.id)
.pipe(finalize(() => {
abp.notify.info("删除通讯录: " + book.name);
this.ngOnInit();
}))
.subscribe(() => { });
}
});
}
}
序号
姓名
邮箱
电话
{{l('Actions')}}
{{(i+1)}}
{{data.name}}
{{data.emailAddress}}
{{data.tel}}
操作
- 修改
-
删除
新增界面代码
创建通讯录
姓名
邮箱
电话号码
import { Component, OnInit,Injector } from '@angular/core';
import { NzModalRef, NzMessageService } from 'ng-zorro-antd';
import { _HttpClient } from '@delon/theme';
import { TelephoneBookServiceProxy, TelephoneBookDto, TelephoneBookListDto } from '@shared/service-proxies/service-proxies';
import { AppComponentBase } from '@shared/app-component-base';
import * as _ from 'lodash';
import { finalize } from 'rxjs/operators';
@Component({
selector: 'books-create',
templateUrl: './create.component.html',
})
export class BooksCreateComponent extends AppComponentBase implements OnInit {
book: TelephoneBookDto = null;
saving: boolean = false;
constructor(injector: Injector,
private _telephoneBookService:TelephoneBookServiceProxy,
private modal: NzModalRef,
public msgSrv: NzMessageService,
private subject: NzModalRef,
public http: _HttpClient
) {
super(injector);
}
ngOnInit(): void {
this.book = new TelephoneBookDto();
// this.http.get(`/user/${this.record.id}`).subscribe(res => this.i = res);
}
save(): void {
this.saving = true;
this._telephoneBookService.createOrUpdate(this.book)
.pipe(finalize(() => {
this.saving = false;
}))
.subscribe((res) => {
this.notify.info(this.l('SavedSuccessfully'));
this.close();
});
}
close() {
this.subject.destroy();
}
}
编辑页面代码
编辑通讯录
姓名:{{book.name}}
邮箱
电话号码
import { Component, OnInit,Injector,Input } from '@angular/core';
import { NzModalRef, NzMessageService } from 'ng-zorro-antd';
import { _HttpClient } from '@delon/theme';
import { TelephoneBookServiceProxy, TelephoneBookDto, TelephoneBookListDto } from '@shared/service-proxies/service-proxies';
import { AppComponentBase } from '@shared/app-component-base';
import * as _ from 'lodash';
import { finalize } from 'rxjs/operators';
@Component({
selector: 'books-edit',
templateUrl: './edit.component.html',
})
export class BooksEditComponent extends AppComponentBase implements OnInit {
book: TelephoneBookDto = null;
@Input()
bookId:string = null;
saving: boolean = false;
constructor(injector: Injector,
private _telephoneBookService:TelephoneBookServiceProxy,
private modal: NzModalRef,
public msgSrv: NzMessageService,
private subject: NzModalRef,
public http: _HttpClient
) {
super(injector);
this.book = new TelephoneBookDto();
}
ngOnInit(): void {
// this.book = new TelephoneBookDto();
this._telephoneBookService.getForEdit(this.bookId)
.subscribe(
(result) => {
this.book = result;
});
// this.http.get(`/user/${this.record.id}`).subscribe(res => this.i = res);
}
save(): void {
this.saving = true;
this._telephoneBookService.createOrUpdate(this.book)
.pipe(finalize(() => {
this.saving = false;
}))
.subscribe((res) => {
this.notify.info(this.l('SavedSuccessfully'));
this.close();
});
}
close() {
this.subject.destroy();
}
}
参考资料
- 浅谈命令查询职责分离(CQRS)模式
- 团队开发框架实战—CQRS架构
- DDD 领域驱动设计学习(四)- 架构(CQRS/EDA/管道和过滤器)
- DDD CQRS架构和传统架构的优缺点比较
- CQRS Journey