コマンドとクエリを分離する
LT;DR
CQS: データを返すコード(クエリ)は、データを更新するコード(コマンド)と混在させるべきではない この考え方はデータベースにも適用できる
Read: データベースの状態を変更しないで、有用なデータを返す
Read 以外: データベースの状態を変更するだけで、有用なデータを返さない
データを返す関数は 副作用 (状態の更新)を持つべきではない 副作用を持つ関数はデータを返すべきではない(ユニット型 を返すべき) この考え方もデータベースに適用できる
データストアを「読み込みに最適化されたもの」と「書き込みに最適化されたもの」とで分ける warning.icon 物理的なデータベースを 2 つに分ける必要があるわけではない
論理的にモジュールで分割するのも 1 つの手段
分ける場合、「書き込み」ストアから「読み取り」ストアをコピーするプロセスが必要になる
これがめちゃくちゃ大変なので、メリットと見合うか判断する必要がある
hr.icon
コマンドクエリ分離(CQS)
そこで、 ストレージ 自身もイミュータブルなオブジェクトとして考える つまり、ストレージ内のデータを変更するたびに、新しいバージョンのデータが返される
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 以外: データベースの状態を変更するだけで、有用なデータを返さない
FP に CQS を適用する
FP における CQS は、以下のように表現できる データを返す関数は 副作用 (状態の更新)を持つべきではない 副作用を持つ関数はデータを返すべきではない
上記をデータベースに応用する
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. パフォーマンス上の理由から、複数 エンティティ を一度に返すクエリが存在する 別の型に分けると、それぞれを別のモジュールに分ける設計が導かれる
一方がクエリ(ReadModel)モジュール、もう一方がコマンド(WriteModel)モジュール
code:fsharp
type CreateCustomer = WriteModel.Customer -> DbResult<Unit>
type ReadCustomer = CustomerId -> DbResult<ReadModel.Customer>
CQRS とデータベース分離
データストアを 2 種類準備する
読み込みに最適化されたもの: 非正規化、多量のインデックス、etc...
書き込みに最適化されたもの: インデックス無し、トランザクションあり、etc...
https://scrapbox.io/files/66b5af075af872001c228baf.png
warning.icon 物理的なデータベースを 2 つに分ける必要があるわけではない
2 つに分けない場合の例
「書き込み」モデルは単純なテーブル
「読み取り」モデルはそのテーブルに対する定義済みの ビュー 物理的に分けると、「書き込み」ストアから「読み取り」ストアをコピーする特別なプロセスが必要になる
「読み取り」ストアのデータが「書き込み」ストアのデータと比べて古くなる可能性がある
データストアを分けることによる設計上のメリットが見合うかどうかを判断する必要がある
イベントソーシング
状態に変化があるたびに、その変化をイベントを永続化する
状態そのものを変更するのではない