---
tags: '閱讀筆記'
---
# CQRS ( Command Query Responsibility Segregation )

- 作者: AJAY KUMAR
- TSYS
- 金融公司
- 美國哥倫布
## What
- 由來
- Greg Young (2010)
- 基於 Bertrand Meyer's CQS
- Command vs Query
- Command
- returns void
- 有side-effects
- Query
- returns non-void
- 無side-effects
- CQS vs CQRS
- CQS
- Method 分Command 和 Query
- CQRS
- Model + Method 分Command 和 Query
## Why
- Scalability
- Performance
- 針對讀寫做不同調校
- 全文搜索
- elasticsearch
- ORM 寫 / raw SQL 讀
- view建立,最佳化queries (EX: create index)
- Simplicity
## 程式結構 (DDD)
### Interface
- CRUD-based interface
- 複雜度容易成長
- Too many features in a single method
- Lack of ubiquitous language(共通語言)
- 破壞用戶體驗
- EX: 建立學生資料和註冊學校變成兩個按鈕
- Task-based interface
- Result of identifying each task the user can accomplish with an object
- 沒有 CRUD-based interface 的缺點
- anemic domain models
- 只有屬性,沒有非get/set方法的model
- Untangling 方法
- update method
- DRY
- ✔️Domain knowledge duplication
- 結構相同也不是壞事
- 🔺Don't repeat yourself
- create and delete methods
- DTO名稱符合專家用語
- 刪除DTO沒使用到屬性
- ex: 建立DTO不用 id
- 如果需求不複雜,不須長期維護,可直接用CRUD-based interface
### Commands and queries in CQRS
- 有別於CQS,CQRS其實要回傳done訊息
- Commands
- imperative tense
- tell the application to do something
- should use ubiquitous language
- Queries
- start with "Get".
- ask the application about something
- Events
- past tense
- inform external applications
- 參考圖

### Onion Architecture
- command
- push model
- event
- pull model
- domain model isolation
- 參考圖
- 
- 
- 舉例
- 錯誤例子
- StudentController
- StudentService
- StudentRepository
- 修正
- StudentController ---> EditPersonalInfoCommand +EditPersonalInfoCommandHandler
- 使用Spring (p105)
- 標記`@CommandHandler` 來跟 `@RestController` 區別
- 非Spring
- `EditPersonalInfoCommandHandler handle = new EditPersonalInfoCommandHandler()`
### Decorator Pattern
- Spring 用AOP實作
- command handlers
- 處理商業邏輯
- decorators
- technical issues
- ex: 連線retry
### ⭐Simplifying the read model
- DDD同時用於讀跟寫
- 過於複雜
- 影響效能
- read 不要用Domain Moddel
- 不用封裝(encapsulation)
- 不要DDD
- 不用DTO
- 直接對應view model
- Teddy 心得
- http://teddy-chen-tw.blogspot.com/2020/09/clean-architecturecqrs-pattern.html
- 不用abstractions (p138)
- 不用ORM
- 可以用DB分離改善效能
## DB
### Separate
- 使用時機
- 使用者查詢次數 > 使用者變更資料次數
- 方法
- 分DB
- Command DB
- 高度正規化 ( third normal form)
- Query DB
- 低規正規化(denormalized (first normal form),以能方便查詢為主
- minimizes the amount of joins
- 成本高
- Eventual consistency
- 不用維護分離DB方案
- indexed view
- 不用真的把DB分離
- data level
- Elasticsearch
- CDC
- master and replica
- ⭐CQRS can be just as effective with a only a single database
### Synchronizing
- Asynchronous State-driven projections
- 示意圖
- 
- 方法
- DDD加入Flag機制
- 步驟
- 資料表
- 1. A flag per each aggregate
- 背景執行更新
- 2. Flags in data tables
- 減少壓力,可以把flag 換成其他table ex: student
- 示意圖
- 
- 作更新機制
- domain model 放flag
- 不是 aggregate model
- Event listeners 現有技術
- NHibernate
- Change Tracker in Entity Framework
- DB trigger
- trigger 更新flag
- 不用改source code (指加flag屬性)
- 刪除要加IsDeleted 欄位,不能實際刪除
- Synchronous state-driven projections
- 要等同步回傳OK
- 如果是真的強烈需求才需要實現
- Synchronous projections don't scale
- 建立Indexed views是一種同步方式
- Event-driven projections
- ⭐Cannot rebuild the read database
- Scales really well
- Can use a message bus
- DDD不能存有狀態
- Event-driven 要搞CQRS必須用event sourcing
- 用log重建卻有很大開銷
- Versioning
- 示意圖
- 
- 高頻交易不能使用 (high-frequency stock trading)
- aggregate 加上版本
- optimistic concurrency control.
- CAP
- writes
- Choose consistency and availability
- read
- Choose availability and partitioning
## 誤解
- CQRS and Event Sourcing不是綁定的
- CQRS 沒有event sourcing 也能帶來很大的效益
- Event Sourcing 沒有結和CQRS ,是一個scale差的方案
- 所以通常要用Event Sourcing 也要用CQRS
- commands and queries from handlers
- command 不能發出新command
- command 只能由client觸發
- 多個command 如果有用到重複code ,請獨立新的domain service
- query handler 處理查詢是可以的,只是不建議那麼做
## 參考資料
- https://ithelp.ithome.com.tw/articles/10273154?sc=iThelpR
- https://github.com/benatespina/book-notes/blob/master/cqrs_command_query_responsibility_segregation.md
- https://hackmd.io/@hellocrab/CQRS-reading-report
- DDD 實作
- https://docs.microsoft.com/zh-tw/dotnet/architecture/microservices/microservice-ddd-cqrs-patterns/apply-simplified-microservice-cqrs-ddd-patterns
- https://github.com/ddd-by-examples/all-things-cqrs
- https://newgoodlooking.pixnet.net/blog/post/125501007
- Greg Young
- http://cqrs.files.wordpress.com/2010/11/cqrs_documents.pdf
- https://wenku.baidu.com/view/a1266349360cba1aa811daf6 (翻譯)
- https://topic.alibabacloud.com/tc/a/classic-application-system-structure-cqrs-and-event-tracing_8_8_32312441.html
- http://m-r.azurewebsites.net/index.html#/
- https://zhuanlan.zhihu.com/p/398911340 (演講翻譯)
- https://herbertograca.com/2017/10/19/from-cqs-to-cqrs/ (圖)
- 
## CODE
```java
public class EditPersonalInfoCommand {
public long id;
public String name;
public String email;
//setters & getters
}
```
- StudentController.java : (這樣不好)
```java
public void editPersonalInfo(){
Command command = new EditPersonalInfoCommand();
command.execute();
}
```
```java
interface ICommandHandler{
public Result handle(ICommand command);
}
```
- EditPersonalInfoCommandHandler
```java
class EditPersonalInfoCommandHandler implements ICommandHandler{
private UnitOfWork unitOfWork;
public EditPersonalInfoCommandHandler(UnitOfWork unitOfWork){
this.unitOfWork = unitOfWork;
}
@Override
public Result handle(EditPersonalInfoCommand command){
Repository studentRepository = new StudentRepository(unitOfWork);
Student student = studentRepository.getById(command.getId());
if (student == null)
return Result.fail("No student found with Id '{id}'");
student.setName(command.getName());
student.setEmail(command.getEmail());
unitOfWork.commit();
return Result.oK();
}
}
```
- StudentController.java :
```java=
@Autowired
private UnitOfWork unitOfWork;
@PutMapping("{id}")
public IActionResult editPersonalInfo(@PathParam("id") long id,
@RequestBody StudentPersonalInfoDto dto){
EditPersonalInfoCommand command = new EditPersonalInfoCommand();
command.setEmail(dto.getEmail());
command.setName(dto.getName());
command.setId(id);
EditPersonalInfoCommandHandler handler
= new EditPersonalInfoCommandHandler(unitOfWork);
Result result = handler.handle(command);
return result.isSuccess ? ok() : Error(result.error());
}
```
- EditPersonalInfoCommand
```java
public class EditPersonalInfoCommand implemetns ICommand{
private long id ;
private String name; // FirstName LastName
private String email;
private EditPersonalInfoCommand(long id, string name, string email){
this.d = id;
this.name = name;
this.email = email;
}
}
```
- Student.java
```java
@Entity
public class Student extends Person{
private String grade;
@OneToMany
private List<Course> courses;
}
```
### New Requirement: Database Retries
- EditPersonalInfoCommandHandler.java
```java=
public Result handle(EditPersonalInfoCommand command) {
for (int i = 0; i < 3; i++) {
try {
Repository studentRepo = new StudentRepository(unitOfWork);
Student student = studentRepo.getById(command.getId());
if (student == null)
return Result.fail("No student found with Id '{id}'");
student.setName(command.getName());
student.setEmail(command.getEmail());
unitOfWork.commit();
return Result.oK();
} catch (Exception e) {
continue;
}
}
}
```
- DatabaseRetryDecorator
```java
public class DatabaseRetryDecorator implements ICommandHandler {
private ICommandHandler handler;
private Config config;
public DatabaseRetryDecorator(ICommandHandler handler, Config config) {
config = config;
handler = handler;
}
public Result handle(ICommand command){
for (int i = 0; ; i++){
try{
Result result = handler.handle(command);
return result;
}catch (Exception ex){
if (i >= config.getNumberOfDatabaseRetries() || !isDatabaseException(ex))
throw new Exception();
}
}
}
private boolean isDatabaseException(Exception exception) {
String message = exception.getInnerException().getMessage();
if (message == null)
return false;
return message.contains("The connection is broken and recovery is not possible") || message.contains("error occurred while establishing a connection");
}
}
```
- 另一個Decorator
``` java
public class AuditLoggingDecorator implements ICommandHandler {
private ICommandHandler handler;
public AuditLoggingDecorator(ICommandHandler handler) {
handler = handler;
}
public Result Handle(ICommand command) {
String commandJson = JsonConvert.serializeObject(command);
Console.witeLine("Command of type {command.getType().getName()}:{commandJson}");
return handler.handle(command);
}
}
```
- 調用方式
```java
EditPersonalInfoCommand command = new EditPersonalInfoCommand();
ICommandHandler handler = new DatabaseRetryDecorator(new EditPersonalInfoCommandHandler());
Result result = handler.handle(command);
```
### ORM N+1
```java
public interface StudentRepository extends JpaRepository<Student, UUID> {
}
```
```java
CriteriaBuilder queryBuilder = em.getCriteriaBuilder();
CriteriaQuery<Employees>
query = queryBuilder.createQuery(Employees.class);
Root<Employees> r = query.from(Employees.class);
query.where(
queryBuilder.like(
queryBuilder.upper(r.get(Employees_.lastName)),
"WIN%"
)
);
List<Employees> emp = em.createQuery(query).getResultList();
for (Employees e: emp) {
// process Employee
for (Sales s: e.getSales()) {
// process sale for Employee
}
}
```
### Don't reuse command handlers
```java
class UnregisterCommandHandler implements ICommandHandler{
@Autowired
private Gate gate;
@Autowired
private SessionFactory sessionFactory;
public UnregisterCommandHandler(SessionFactory sessionFactory){
sessionFactory = sessionFactory;
}
public Result handle(UnregisterCommand command){
UnitOfWork unitOfWork = new UnitOfWork(sessionFactory);
Repository repository = new StudentRepository(unitOfWork);
Student student = repository.getById(command.Id);
if (student == null)
return Result.fail($"No student found for Id {command.getId()}");
gate.dispatch(new DisenrollCommand(command.getId(), 0, "Unregistering"));
gate.dispatch(new DisenrollCommand(command.getId(), 1, "Unregistering"));
repository.delete(student);
unitOfWork.commit();
return Result.ok();
}
}
```
## sample code 參考
- https://github.com/davidikin45/Cqrs