--- tags: '閱讀筆記' --- # CQRS ( Command Query Responsibility Segregation ) ![](https://i.imgur.com/R8JAfNJ.png) - 作者: 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 - 參考圖 ![](https://i.imgur.com/t8Cen5L.png) ### Onion Architecture - command - push model - event - pull model - domain model isolation - 參考圖 - ![](https://i.imgur.com/6hIrPKI.png) - ![](https://i.imgur.com/ChjlqnD.png) - 舉例 - 錯誤例子 - 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 - 示意圖 - ![](https://i.imgur.com/WhF63sE.png) - 方法 - DDD加入Flag機制 - 步驟 - 資料表 - 1. A flag per each aggregate - 背景執行更新 - 2. Flags in data tables - 減少壓力,可以把flag 換成其他table ex: student - 示意圖 - ![](https://i.imgur.com/jiSKnJ0.png) - 作更新機制 - 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 - 示意圖 - ![](https://i.imgur.com/BHpcipX.png) - 高頻交易不能使用 (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/ (圖) - ![](https://i.imgur.com/6k1pVLk.png) ## 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