コマンドとクエリを分離する
from DMMF: Persistence
LT;DR
CQS: データを返すコード(クエリ)は、データを更新するコード(コマンド)と混在させるべきではない
この考え方はデータベースにも適用できる
Read: データベースの状態を変更しないで、有用なデータを返す
Read 以外: データベースの状態を変更するだけで、有用なデータを返さない
FP に CQS の考え方を適用すると、CQS は以下のように表せる
データを返す関数は 副作用 (状態の更新)を持つべきではない
副作用を持つ関数はデータを返すべきではない(ユニット型 を返すべき)
CQRS: クエリ の モジュール と コマンド のモジュール分離する
OOP だと別々の インタフェース を作成するイメージ radish-miyazaki.icon
この考え方もデータベースに適用できる
データストアを「読み込みに最適化されたもの」と「書き込みに最適化されたもの」とで分ける
warning.icon 物理的なデータベースを 2 つに分ける必要があるわけではない
論理的にモジュールで分割するのも 1 つの手段
分ける場合、「書き込み」ストアから「読み取り」ストアをコピーするプロセスが必要になる
これがめちゃくちゃ大変なので、メリットと見合うか判断する必要がある
永続化 には、イベントソーシング のような手法が取られることもある
hr.icon
コマンドクエリ分離(CQS)
FP による ドメインモデリング では、すべてのオブジェクトは イミュータブル として設計される
I/O を端に追いやる#6690982075d04f00002d5c49
そこで、 ストレージ 自身もイミュータブルなオブジェクトとして考える
つまり、ストレージ内のデータを変更するたびに、新しいバージョンのデータが返される
Create 操作のイメージ図
https://scrapbox.io/files/66b5a33263e595001d8fa099.png
他の操作(Read, Delete, Update)についても同様に考えられる
https://scrapbox.io/files/66b5a36e09da6a001c687420.png
コードで表現する
code:fsharp
type CreateData = DataStoreState -> Data -> NewDataStoreState
type ReadData = DataStoreState -> Query -> Data
type UpdateData = DataStoreState -> Data -> NewDataStoreState
type DeleteData = DataStoreState -> Key -> NewDataStoreState
このうち、Read とそれ以外でグループ分けすることができる
Read: データベースの状態を変更しないで、有用なデータを返す
Read 以外: データベースの状態を変更するだけで、有用なデータを返さない
この区別に基づいた設計思想が CQS
FP に CQS を適用する
FP における CQS は、以下のように表現できる
データを返す関数は 副作用 (状態の更新)を持つべきではない
副作用を持つ関数はデータを返すべきではない
ユニット型 を返す関数であるべき
上記をデータベースに応用する
最初に、コマンドとクエリを分離する#66b5a4b475d04f0000675a81 の型シグネチャを改善する
DataStoreState を DbConnection など、データストアへの何らかのハンドルに置き換える
実際のデータストアは ミュータブル であり新しい状態を返さないので、Unit で置き換える
code:fsharp
type CreateData = DbConnection -> Data -> Unit
type ReadData = DbConnection -> Query -> Data
type UpdateData = DbConnection -> Data -> Unit
type DeleteData = DbConnection -> Key -> Unit
ただし、DbConncetion は特定のデータストアに固有なので、部分適用などを使って呼び出し側から隠蔽するのが良い
code:fsharp
type CreateData = Data -> Unit
type ReadData = Query -> Data
type UpdateData = Data -> Unit
type DeleteData = Key -> Unit
また、非同期処理やエラーなども発生しうるので、エフェクト を考慮する必要がある
code:fsharp
type DbError = ...
type DbResult<'a> = AsyncResult<'a,DbError>
type CreateData = Data -> DbResult<Unit>
type ReadData = Query -> DbResult<Data>
type UpdateData = Data -> DbResult<Unit>
type DeleteData = Key -> DbResult<Unit>
コマンドクエリ責務分離(CQRS)
クエリから返ってくるデータ と コマンドに渡すデータを同じ型にすることができる
code:fsharp
type CreateCustomer = Customer -> DbResult<Unit>
type ReadCustomer = CustomerId -> DbResult<Customer>
しかし、以下の理由から避けたほうが良い(別の型として定義したほうが良い)
1. これらのデータは異なることが多い
クエリは 非正規化 されたデータや計算値を返す可能性があるが、コマンドでは利用しない
クエリは ID フィールドを返すが、コマンドのデータには含まれない(含められない)
2. クエリとコマンドは独立して進化する傾向がある
e.g.
クエリ: 注文を取得した後で関連する顧客データを取得するのではなく、まとめて取得する
コマンド: 顧客への参照(CustomerId)のみを利用する
3. パフォーマンス上の理由から、複数 エンティティ を一度に返すクエリが存在する
別の型に分けると、それぞれを別のモジュールに分ける設計が導かれる
OOP だと、インタフェース を分離する radish-miyazaki.icon
一方がクエリ(ReadModel)モジュール、もう一方がコマンド(WriteModel)モジュール
これを CQRS と呼ぶ
code:fsharp
type CreateCustomer = WriteModel.Customer -> DbResult<Unit>
type ReadCustomer = CustomerId -> DbResult<ReadModel.Customer>
CQRS とデータベース分離
CQRS を データベースにも適用できる
データストアを 2 種類準備する
読み込みに最適化されたもの: 非正規化、多量のインデックス、etc...
書き込みに最適化されたもの: インデックス無し、トランザクションあり、etc...
https://scrapbox.io/files/66b5af075af872001c228baf.png
warning.icon 物理的なデータベースを 2 つに分ける必要があるわけではない
2 つに分けない場合の例
「書き込み」モデルは単純なテーブル
「読み取り」モデルはそのテーブルに対する定義済みの ビュー
物理的に分けると、「書き込み」ストアから「読み取り」ストアをコピーする特別なプロセスが必要になる
「読み取り」ストアのデータが「書き込み」ストアのデータと比べて古くなる可能性がある
「読み取り」ストアのデータは 結果整合性
ドメイン によっては問題になることもある
異なるコンテキスト間での整合性#669a402675d04f00002de456
データストアを分けることによる設計上のメリットが見合うかどうかを判断する必要がある
イベントソーシング
イベントソーシング: CQRS の文脈でよく見かける 永続化 の手法
状態に変化があるたびに、その変化をイベントを永続化する
状態そのものを変更するのではない
git のような バージョン管理システム のようなイメージ