Read Model 的現狀#
在前幾個 Module 中,我們已經將 Read 和 Write 在 API、Application Services、Domain Model 等層級進行了分離。然而,資料層(Data Level) 仍然共用同一個資料庫。這個 Module 的目標是完成最後一步:為 Queries 引入獨立的資料庫,從而完整實作 CQRS pattern。
Commands 資料庫作為所有資料的 master storage,Queries 資料庫與之同步後,獨立提供讀取服務。這樣可以分散 Commands 端的壓力。
CQRS 本身只提供 Queries 端的擴展指引。若要擴展 Commands 端,則需要引入 sharding,但這在大多數企業級應用中相當罕見,因為讀取量通常遠超寫入量。
Domain Model 的分離#
資料層的分離在現實世界中的應用#
現實中有許多 CQRS 在資料層分離的範例,而且你不一定需要自行建構:
- Indexed View:在 SQL Server 中建立 indexed view,資料庫伺服器會自動提供所需的基礎設施
- Database Replication:一個 master 資料庫搭配多個 replicas,master 負責寫入,replicas 負責讀取。資料庫軟體自動處理資料同步
- ElasticSearch:全文搜尋引擎,透過索引關聯式資料庫的資料,提供豐富的查詢能力
在自行實作分離之前,優先考慮利用現有的資料庫功能。這些開箱即用的方案可以節省大量工作。
為 Queries 設計專用資料庫#
開箱即用的資料庫功能雖然方便,但有其侷限性。例如 database replication 產生的 replica 與 master 具有相同的結構,但 reads 和 writes 有不同的需求,相同的 schema 不一定適合 Read side。
以範例應用的 SQL 查詢為例,原本需要從 Student、Enrollment、Course 三張 table 中 JOIN 取得資料:
SELECT s.*, e.Grade, c.Name CourseName, c.Credits
FROM dbo.Student s
LEFT JOIN (
SELECT e.StudentID, COUNT(*) Number
FROM dbo.Enrollment e
GROUP BY e.StudentID) t ON s.StudentID = t.StudentID
LEFT JOIN dbo.Enrollment e ON e.StudentID = s.StudentID
LEFT JOIN dbo.Course c ON e.CourseID = c.CourseID
WHERE (c.Name = @Course OR @Course IS NULL)
AND (ISNULL(t.Number, 0) = @Number OR @Number IS NULL)
ORDER BY s.StudentID ASC透過 denormalization,我們可以將 Queries 資料庫簡化為單一的 flat table,其 schema 完全匹配 Read side 的需求:
StudentID、Name、EmailNumberOfEnrollmentsFirstCourseName、FirstCourseCredits、FirstCourseGradeSecondCourseName、SecondCourseCredits、SecondCourseGrade

Schema Denormalization:將正規化的多張資料表(Student、Enrollment、Course)轉換為單一的 Denormalized flat table
簡化 Read Model#
建立 Queries 專用資料庫#
建立獨立的 Queries 資料庫後,需要處理 connection string 的設計:
- 將原本的
ConnectionString拆分為CommandsConnectionString和QueriesConnectionString - 不要將兩個 connection string 放在同一個 class 中,這會違反 Interface Segregation Principle(ISP)
- 使用兩個獨立的 class,因為你永遠不會同時需要兩者,這也能避免混用的錯誤
ORM 只用於 Commands 端。Query handler 使用
QueriesConnectionString直接查詢 Read 資料庫。
簡化後的 Query Handler#
有了 denormalized 的 Read 資料庫,query handler 變得極為簡潔:
public List<StudentDto> handle(GetListQuery query) {
string sql = "SELECT * FROM dbo.Student s
WHERE (s.FirstCourseName = @Course
OR s.SecondCourseName = @Course
OR @Course IS NULL)
AND (s.NumberOfEnrollments = @Number
OR @Number IS NULL)
ORDER BY s.StudentID ASC";
SqlConnection connection = new
SqlConnection(connectionString.getValue());
List<StudentDto> students = connection
.query<StudentDto>(sql,
new { query.getEnrolledIn(), query.getNumberOfCourses() })
.toList();
return students;
}相較於原本的版本,簡化後的 query handler 有以下改進:
- SQL 查詢本身大幅簡化,不再需要多表 JOIN
- 只需要一次資料庫呼叫
- 不需要使用中間 class 進行資料轉換,直接將結果映射到 DTO
回顧:簡化 Read Model#
透過為 Read 和 Write model 套用不同的架構方法,我們達成了以下效果:
- Commands 資料庫遵循 Third Normal Form(3NF),適合交易處理
- Queries 資料庫採用 First Normal Form(1NF),適合查詢操作
- Denormalization 最小化了 JOIN 數量和後處理的工作量

DDD 僅適用於 Commands 端:Queries 端不修改資料,因此不需要封裝與抽象
高正規化形式適合 Commands;低正規化形式適合 Queries。這是企業級應用中常見的 pattern。你也可以進一步使用 document database 等不同的儲存類型,根據 Read model 的需求選擇最合適的方案。
Read Model 與 Onion Architecture#
Scalability#
分離資料庫後,Read side 的擴展變得容易。只需建立更多的 Read 資料庫,並讓它們與 Commands 資料庫同步即可。
- Read model 具有 immutable 特性:不修改狀態、沒有副作用
- 因此可以無限擴展 Read 端
- Command model 的擴展較困難,因為涉及資料變更
多個 Read Model#
一個應用可能需要多個 Read model,例如:
- Web client:需要較完整的資料
- Mobile client:需要較少的資料,且可能需要不同格式
每個 client 可以有自己的 Read 資料庫,擁有獨立的 schema 和同步機制。但如果不同 client 的需求相似,單一 Read model 也足夠。
關於 Read 資料庫的注意事項#
引入獨立的 Read 資料庫需要謹慎考量其成本:
- 兩個資料庫之間的同步會增加大量複雜度
- Eventual consistency 可能導致使用者混淆(例如新建的 student 沒有立即出現在列表中)
- 可以透過 UI 提示來緩解(例如顯示「資料將在幾分鐘內處理完成」)
在大多數情況下,單一資料庫就已經足夠。CQRS pattern 在只使用一個資料庫的情況下也能非常有效。只有在你真正需要擴展系統(特別是 Read side)時,才引入第二個資料庫。
小結#
- 為 Queries 引入獨立資料庫,完成了 CQRS pattern 在所有層級的分離(API、Application Services、Domain Model、Database)
- 現實世界中有多種資料層分離的範例:Indexed View、Database Replication、ElasticSearch
- 透過 denormalization 將 Read 資料庫調整為完全匹配 Query model 的需求,大幅簡化 SQL 查詢和 query handler
- Commands 資料庫適合使用 3NF(高正規化),Queries 資料庫適合使用 1NF(低正規化)
- 不同 client(Web、Mobile)可能需要各自獨立的 Read model 和資料庫
- Read model 的 immutable 特性使其可以無限擴展
- 引入第二個資料庫帶來的 eventual consistency 和維護成本不可忽視,在大多數情況下單一資料庫已足夠