CQRS 與 Event Sourcing#
CQRS 常被描述為通往 Event Sourcing 的踏腳石,彷彿它只是一個過渡階段,最終必然導向 Event Sourced 架構。這是一個誤解。
No segregation -> CQRS -> Event Sourcing
CQRS 可以獨立提供大量價值,即使你從未實作 Event Sourcing。
CQRS 不需要 Event Sourcing#
- CQRS 不需要 Event Sourcing 即可運作
- Event Sourcing 通常需要 CQRS 才能有效運作
- Event Sourcing 沒有搭配 CQRS(不分離讀寫)是較不常見且較不具擴展性的方案
Event Sourcing 帶來大量的複雜度,其門檻遠高於 CQRS,甚至高於資料庫層級的分離。雖然 Event Sourced 系統仍有其價值,但複雜度的代價不像 CQRS 那樣容易被合理化。
適合 Event Sourcing 的場景#
通常受益於 Event Sourcing 的系統是一種特殊類型 – 那些必須追蹤導致特定狀態的所有事件的系統:
- 金融科技(Finance tech):必須追蹤所有事件,提供所有金融交易的 audit log
- 在這類系統中,將 domain events 視為一等公民非常合理
CQRS 可以獨立於 Event Sourcing 成功運用。這兩個概念大致上是正交的(orthogonal),但並非完全無關。
Evolutionary Design(演進式設計)#
演進式設計是一個重要概念:不需要一次性導入所有 CQRS 相關技術。
- 不必實作課程中的所有技術
- 延遲決策,直到需求被證明確實存在
- 這個原則適用於任何 design pattern,不僅限於 CQRS
每個 design pattern 都有其成本與效益。在套用模式之前,務必確保效益大於成本。
CQRS 不是全域模式#
CQRS 不是像 Microservices 架構那樣的應用程式層級模式,而是應該綁定到特定的 Bounded Context 或 Microservice。這意味著:
- 在不同的 Bounded Context 中可以做出不同的 CQRS 決策
- 對業務至關重要且有高效能需求的 Bounded Context,可以實作到資料層級的完全分離
- 較不重要的 Bounded Context,可以僅在 API 層級做分離
- 甚至保留 CRUD 導向的 endpoints 也可以是一種選項
在大多數企業級應用中,甜蜜點(sweet spot)是分離到 Domain Model 層級 – 分離除了資料庫以外的一切。這通常能在成本與效益之間取得最佳平衡。
從 Handlers 使用 Commands 和 Queries#
一個常見的問題是:能否從 Command Handler 或 Query Handler 中使用其他的 Commands 和 Queries? 簡短的回答是「不行」,但詳細的答案取決於具體情境。
Command Handler 中使用另一個 Command:不行#
以下是一個反面範例,在 UnregisterCommandHandler 中 dispatch 了 DisenrollCommand:
class UnregisterCommandHandler implements ICommandHandler {
@Autowired
private SessionFactory sessionFactory;
@Autowired
private Gate gate;
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.Id);
gate.dispatch(new DisenrollCommand(student.getId(), 0, "Unregistering"));
gate.dispatch(new DisenrollCommand(student.getId(), 1, "Unregistering"));
repository.delete(student);
unitOfWork.commit();
return Result.Ok();
}
}應用程式不應該自行觸發 Commands。 Command 代表客戶端對應用程式能做的事情,只有客戶端才能發起 Command。系統回應 Command 並產生 Domain Events,而不是產生新的 Command。
如果需要在 Command Handlers 之間重用邏輯,應該將共用程式碼提取到一個獨立的類別中,例如 Domain Service,而不是重用 Command Handler 本身。
訊息循環的概念#
- Command:客戶端對應用程式能做什麼
- Event:應用程式發生了什麼事
- State:所有 Events 的總和
客戶端看到應用程式的當前狀態並產生 Command。Write Model 接收 Command 並產生 Events。Read Model 接收 Events 並建構當前狀態的快照,然後呈現給客戶端。
Command Handler 中使用 Query:視情況而定#
這取決於你是否能在 Command 端可靠地取得當前狀態:
- 如果可以(例如沒有使用 Event Sourcing):直接查詢 Command 資料庫即可,不需要使用 Query
- 如果不行(例如使用了 Event Sourcing):由於無法從 Event Log 有效率地重建狀態,你別無選擇,只能使用 Read 資料庫透過 Query 來取得資料
在 Event Sourcing 場景下使用 Read 資料庫會帶來一系列一致性問題,因為 Read 和 Write 資料庫之間不是立即一致的。例如,unique constraint 驗證可能因此無法正確執行。
Query Handler 中使用 Command 或 Query#
- 使用 Command:絕對不行,因為 Query Handler 不能改變狀態
- 使用另一個 Query:由於 Read Model 是不可變的(immutable),指導原則沒有那麼嚴格。但一般來說,仍然建議不要重用 Query Handlers,而是將共用邏輯提取到獨立的 Service 中
| Command | Query | |
|---|---|---|
| Command Handler | 不行 | 僅限 Event Sourcing 場景 |
| Query Handler | 不行 | 盡量避免 |
One-way Commands(單向 Commands)#
另一個常見的誤解是 Commands 應該永遠是單向的 – 也就是說 Commands 不應該回傳任何東西,因為這會違反 CQS(Command-Query Separation)原則。這個觀點建議改為輪詢(poll)一個外部來源來取得 Command 執行結果。
這是一個誤解。
真正的 One-way Commands 是不可能的#
Commands 總是需要回傳某種形式的確認:
- OK 或 Error
- 一個 locator(可用來從另一個 API endpoint 輪詢結果)
既然如此,如果操作本身不是天生非同步的,為什麼要回傳 locator 而不直接回傳結果呢?
Commands 可以回傳的內容#
- 如果操作不需要太多時間,同步完成並回傳 OK 或 validation error
- 如果是更新操作,可以回傳 aggregate 的新版本號
- 如果是建立操作,可以回傳新建實體的 ID
回傳新建資源的 ID 看起來像是違反了 CQS 原則,但其實不然。Commands 回傳它們所建立的資源的 locator 是可以接受的。如果你同步建立資源,那麼該資源的 ID 就是這個 locator。
CQRS vs. Specification Pattern#
這是兩個 Domain-Driven Design 模式之間的一個重要衝突。
Specification Pattern 的用途#
Specification Pattern 將一段領域知識封裝在單一位置,然後在三種場景中重用:
- Data retrieval(資料檢索)– 屬於 Query 端
- Input validation(輸入驗證)– 屬於 Command 端
- Construction-to-order(按需建構)
衝突所在#
- Specification Pattern 主張使用單一 Domain Model 來處理驗證和資料檢索,遵循 DRY 原則(Don’t Repeat Yourself)
- CQRS 主張將 Domain Model 拆分為兩個,分別處理讀和寫,追求 Loose Coupling
如何取捨?#
這是 DRY 原則與 Loose Coupling 原則之間的經典衝突。指導原則如下:
- Loose Coupling 在絕大多數情況下勝出,除了最簡單的情境
- Domain Knowledge 重複是較小的惡:重複領域知識雖然不太方便,但與高耦合的替代方案相比並不算糟糕
- 高耦合帶來太多限制:你的雙手會被另一個元件的限制所束縛
在大型系統中,你幾乎總是會傾向選擇 Loose Coupling 而非防止讀寫之間的領域知識重複。Specification Pattern 在較簡單的情境中運作良好,但在大型系統中 CQRS 的分離帶來更大的靈活性。
Book Summary(全書總結)#
整本書涵蓋了以下主題:
- CQRS 模式:它是什麼,以及如何在實務中應用
- CQRS 的三大目標:Simplicity(簡單性)、Performance(效能)、Scalability(可擴展性),其中簡單性最為重要
- 漸進式引入分離:
- 分離 API endpoints,將單一巨大的 update method 拆分為多個 task-based 方法
- 提取明確的 Commands、Queries 和 Handlers,引入統一介面和 Decorators
- 從 Read 端移除 Domain Model,直接撰寫高效能 SQL 查詢
- 引入獨立的 Read 資料庫,schema 完全配合 Read Model 需求
- 讀寫資料庫之間的同步
- CQRS 最佳實踐與常見誤解
小結#
本模組總結了 CQRS 的核心最佳實踐與常見誤解:
- CQRS 與 Event Sourcing 是獨立的概念:CQRS 不需要 Event Sourcing 即可提供大量價值,但 Event Sourcing 通常需要 CQRS
- 採用演進式設計:不需要一次導入所有技術,延遲決策直到需求被證明,並確保每個模式的效益大於成本
- 不要從 Handler 中觸發其他 Commands:Command 只能由客戶端發起,需要共用邏輯時應提取到 Domain Service
- One-way Commands 是個誤解:Commands 可以也應該同步回傳結果(OK、Error、新建資源的 ID),不需要強迫使用非同步輪詢
- CQRS 優先於 Specification Pattern:在 DRY 原則與 Loose Coupling 之間,大型系統幾乎總是選擇 Loose Coupling,容忍少量的領域知識重複
- CQRS 是 Bounded Context 層級的決策,不同的 Context 可以有不同的分離程度