
圖片來源:ChatGPT 生成
如果沒記錯,應該是在《CQRS 命令查詢職責分離模式》這本書第一次看到 Read Model 這個術語,但不確定是我讀的是翻譯本,還是原本就沒有,書中其實沒有對 Read Model 有明確的定義,就我的理解,導入 Read Model 有幾個目的:
- 與領域模型分離,領域模型只處理複雜的關連與邏輯,避免負擔太多責任
- 讀取模型能善用特定資料庫提供的功能最佳化效能
書中不少篇幅討論 DTO (Data Transfer Object) 與領域模型和讀取模型的關係,有趣的是,我對 DTO 的理解是從《Patterns Of Enterprise Application Architecture》這本書來的 (粗體是我認為重點的部分):
An object that carries data between processes in order to reduce the number of method calls.本文中,DTO 就只是一個為了減少函式呼叫次數用來傳遞資訊的物件,和如今作為跨 boundary 傳遞資料的物件不太一樣。那今天到底要討論什麼?Read Model 還是 DTO?不急,先看怎麼使用,再來回顧究竟屬於什麼吧。
在一開始的架構設計之初,就把主管用的後台管理和員工使用的 app 各別提供一個後端 (Backend) [1],針對不同的應用提供專屬的 APIs,以目前的情況來說,後台管理使用的 APIs 操作的物件都比較接近領域模型,因為後台存在的目的就是用來管理這些複雜的領域模型。

圖片來源:ChatGPT 生成
後台操作介面需要的資料分成 metadata 和一般資料,後台操作介面也能編輯 metadata 和一般資料。提供一個物件將畫面所需要的資料都包起來,用一次 API 取得,聽起來好像不錯,但由於二者的更新頻率差異很大,這麼一大包資料的傳輸反而太花時間,由前端個別管理這些資料,只更新有變動的資料,再讓畫面根據變動更新即可,因此後台管理介面的後端幾乎沒有用到這類物件。
員工的 app 大多數時間只是檢視主管操作後的結果,雖然 metadata 仍可能因為主管編輯而變動,但資料卻是發布後員工才能看到的,在發布前任何操作的異動員工是無法得知,導致在員工端,metadata 和一般資料的更新頻率非常接近,這時一次 API 把畫面需要的資料全部取回反而是很划算的選擇。
這時候 Java 新的 record 真的幫上大忙,宣告 read-only 的大利器,這樣便宣告了一個 value object,還自動生成了 getter 和 equals 等函式:
ConnectedDailyNote 中,SimpleSquad 和 SimpleMerchant 都是對應 Squad 和 Merchant 的簡化版,只回傳了 app 需要的部分,app 不需要再次呼叫 API,只為了取得對應的內容。更重要的是,這些資料可以在 SQL statement 層級優化並取回,大大地提升效率。
上述的例子,還只是一個很簡單的例子,在實際系統中,有的資料結構由更多的簡化版 entity 組成,如果 app 都是各自取得這些資料,若不想頻繁呼叫 API,那可能要建立一個複雜的快取管理。透過這樣的資料結構,讓 app 取得即可呈現,某種程度簡化 app 的開發。
雖然導入這樣的資料結構不難,但使用上還是要注意一些細節:
- 如果是 SQL 層級的優化,任何 schema 的改變都可能需要同時修改查詢條件,不然會造成查詢出現錯誤,因此一定要有單元測試,而且一定要使用相同 schema 測試 [2],不然會出現測試通過,但實際出錯的現象。
- 如果是更複雜的資料結構,或是資料的背後是不同的系統,無法單靠一次查詢就組合出想要的資料結構,那就要思考如何優化查詢邏輯,減少查詢的次數。
- 雖然 SQL 層級的優化可以加速,但某種程度上是可能打破模組的邊界,導致不是透過模組公開的介面讀取資料,這取捨得由團隊決定,只能透過公開介面讀取資料依然是個非常好的原則,個人的底線是,只能透過公開介面「修改」資料,其他唯讀操作視情況可以放行。
好啦!回到開頭討論的 Read Model 和 DTO,大家覺得 ConnectedDailyNote 是 Read Model 還是 DTO 呢?個人覺得是減少 API 呼叫次數且針對 SQL 優化的讀取模型,很貪心吧 (笑)!
可能有人會提到 GraphQL 也能做到類似的設計,沒錯,但本文討論的是一種設計的思維,透過把複雜的關聯維護邏輯 (Entity) 與讀取邏輯 (Read Model)分開,讓程式碼與資料結構以合理的方式分別維護,降低系統的複雜度,GraphQL 是一種實現的方式,但即便不用 GraphQL 也可以用這思維設計系統。
最後,留個小問題給大家,Read Model 在有使用 layer 或 clean architecutre 的系統中,應該放在哪一層?
- 這算是 Backend for Frontend 嗎?個人沒有這樣宣稱,當初的考量也不全是為了符合前端需求才分開。
- 這次專案使用 Testcontainers 真的是很方便,簡單的幾個 annotation 就可以在測試開始前準備好 PostgreSQL 的 container 進行測試,測試結束就把 container 消滅掉,讓測試和開發環境各自獨立。














