CQRS 與其起源#

CQRS 全名為 Command-Query Responsibility Segregation。其核心概念極為簡單:不再使用單一的統一模型,而是引入兩個模型——一個負責讀取,另一個負責寫入。

CQRS 由 Greg Young 於 2010 年提出,其理論基礎來自 Bertrand Meyer 所定義的 Command-Query Separation(CQS) 原則。

CQS 原則#

CQS 原則規定:每個方法都應該是 CommandQuery,而非兩者兼具。換句話說,「提問不應改變答案」

  • Command:產生副作用(side-effect),回傳 void
  • Query:無副作用(side-effect free),回傳非 void

遵循此原則,僅透過方法簽名就能判斷其用途,無需深入實作細節,從而提升程式碼的可讀性。

CQS 的限制#

CQS 並非在所有情況下都適用,以下兩種場景可能需要違反此原則:

  • Stack.pop():同時移除元素並回傳值,拆分為兩個方法反而不合理
  • 競態條件(Race Condition):當 Query 結果可能在 Command 執行前過期時,需要將兩者合併以確保操作的原子性
// 遵循 CQS
public bool fileExists(String path) {
    return file.exists(path);
}
public void writeToFile(String path, String content) {
    if (!fileExists(path))
        throw new IllegalArgumentException();
    file.writeAllText(path, content);
}

// 違反 CQS(但確保原子性)
public Result writeToFile(String path, String content) {
    try {
        file.writeAllText(path, content);
        return Result.ok();
    }
    catch (fileNotFoundException e) {
        return Result.fail("File not found");
    }
}

儘管存在例外情況,CQS 仍應作為預設的設計選擇,僅在確有必要時才偏離。

從 CQS 到 CQRS#

CQS 關注的是方法層級的分離,而 CQRS 則將相同的概念提升到模型與類別層級

操作單位
CQSMethod-command / Method-query
CQRSCommand model / Query model

CQRS 鼓勵你將單一的統一 Domain Model 拆解為兩個模型:一個處理 Command(寫入),另一個處理 Query(讀取)。

CQS 與 CQRS 的關係:CQS 作用於方法層級,CQRS 將其擴展到模型與類別層級

為什麼要使用 CQRS#

CQRS 的三大核心效益:

可擴展性(Scalability)#

在典型的企業級應用中,讀取操作遠多於寫入操作。CQRS 允許對讀寫進行非對稱擴展

  • Command 端(Create / Update / Delete):1 台伺服器
  • Query 端(Read):10 台伺服器

由於 Command 與 Query 的處理本質上是不對稱的,非對稱擴展完全合理。

效能(Performance)#

即使讀寫部署在同一台伺服器上,分離後仍可針對 Query 端進行獨立優化:

  • 為查詢 API 設置專屬快取
  • 使用資料庫特定功能手工優化的 SQL
  • 不受 Command 端 ORM 的限制

讀寫分離架構:Write model 與 Read model 各自透過獨立 API 存取資料庫,Query 端可使用 Cache 和高度優化的 SQL

簡潔性(Simplicity)#

CQRS 最重要的效益是簡潔性。Command 端與 Query 端的需求截然不同,試圖用單一模型來滿足兩者,如同「將方形積木塞入圓形孔洞」,最終只會得到一個過度複雜且兩頭不討好的模型。

CQRS 可視為單一職責原則(SRP)在架構層級的應用

  • Commands:負責變更資料
  • Queries:負責讀取資料

透過明確區分兩種不同的使用場景,引入各自獨立的模型,能大幅降低程式碼複雜度。每個模型只做一件事,且做得好。

真實世界中的 CQRS#

你可能已經在不知不覺中使用了 CQRS 模式:

  • ORM 寫入 + Raw SQL 讀取:使用 Hibernate 寫入資料庫,但用 JDBC 原生 SQL 讀取——這就是 CQRS
  • Database Views:為特定的讀取場景建立最佳化的資料庫視圖
  • ElasticSearch:從關聯式資料庫索引資料,提供豐富的全文搜尋能力——寫入模型與讀取模型完全分離

上述場景的共通點是:寫入使用一種模型,讀取使用另一種完全獨立的模型。差別只在於讀取模型是自行建構還是借助既有軟體。

小結#

  • CQRS 源自 CQS 原則,並將其擴展到架構層級
  • CQRS 將統一的 Domain Model 拆分為 Command ModelQuery Model
  • 三大效益:可擴展性效能,以及最重要的——簡潔性
  • CQRS 本質上是 SRP 在架構層級的應用
  • ElasticSearch、Database Views 等都是 CQRS 在真實世界中的實例