コマンド・クエリ責務分離(CQRS)パターン入門

著者 周蕾(ArcBlockバックエンドエンジニア)
ここ数年、DDDの分野ではCQRSアーキテクチャという概念をよく目にします。CQRSはコマンド・クエリ責務分離パターン(Command Query Responsibility Segregation)の略称です。ちょうど最近、ArcBlockのバックエンドサービスでCQRSアーキテクチャの採用を検討しているため、今回は最近の調査で得た知見を皆さんと共有します。本稿ではEvent Sourcingから出発してCQRSを紹介し、Commanded(Elixirのライブラリ)を通じて、ES/CQRSの概念に従ってアプリケーションを開発する方法を見ていきます。
Event Sourcing(イベントソーシング)とは?
Event Sourcingの本質は、現在の物事の状態ではなく、発生したイベントそのものを保存することです。Event Sourcingの概念では、Eventはすでに発生した確定済みの事実であり、最小単位でもあります。 たとえば:

Event Sourcingの動作方式:以下のデータフローは4つの発生済みイベント(event)で構成され、それぞれが現在の状態を変更します。同時に、イベント間の相対的な順序も保証する必要があります。

次のようにまとめられます:
Sn = apply(Sn-1, En) または Sn = reduce(E, S0, apply)
ここで:(S:state、E:Event)
ここから、Event Sourcingには次の利点があることが分かります:
- 各状態の変化について完全なログ記録があり、追跡できる
- 書き込み操作が最適化され、性能が向上する
身近なEvent Sourcing
- すべてのプログラマーが毎日使うGithub。Gitの世界では、Events(イベント)はCommits、State(状態)はファイルシステムです。
- Blockchainが毎回保存するのは現在の状態ではなくtransactionです。この観点では、Events(イベント)はtransaction、State(状態)はユーザーのアカウント情報です。
- WAL:Write-ahead loggingのことで、データをデータベースへ書き込む前にまずログへ書き込み、その後ログの記録内容をストレージへ反映します。Events(イベント)は各操作、State(状態)はデータベースです。
Event Sourcingでクエリ(query)を実行するには?
銀行システムで残高が1000元を超えるユーザーを検索したいとしましょう。その場合、各口座についてSn = reduce(E, S0, apply)という式に従って、もう一度すべて計算し直す必要があるのでしょうか? あるDataStoreでEventを保存し、別のDataStoreをQuery専用のデータ提供に使い、さらに2つのDatastore間でメッセージを送信して情報を同期するという方法はどうでしょうか? CQRSは、ある意味でこのような問題を解決します。
CQRSとは?
CQRSの正式名称はCommand Query Rsponsibility Segregationで、アプリケーションをコマンド側(Command)とクエリ側(Query)の2つに分けます。コマンド側は作成、更新、削除のリクエストを処理し、データが変更されるとイベントを発行します。クエリ側はクエリを実行して照会を処理し、データ変更時に発行されるイベントストリームを購読することで最新の状態を保ちます。CQRSは分離されたインターフェースを使用して、データ照会操作(Queries)とデータ変更操作(Commands)を分けます。これは、照会と更新の過程で使用するデータモデルも異なることを意味します。こうして読み取りと書き込みのロジックが分離されます。

CQRSにおけるいくつかの概念:
- Command(コマンド):結果は返さず、検証に成功するとオブジェクトの状態を変更します。
- Query(クエリ):結果を返しますが、オブジェクトの状態は変更しません。
- Aggregate(集約):状態を保持し、commandを処理し、状態を変更します。
- Event Store:Eventsを保存します。
CQRSパターンに従ってアプリケーションを構築するには?
まず、CQRS/ESパターンに従ってCommand sideを実装するElixirライブラリ、Commandedを利用します。
1. Commands
Commandsはユーザーがアプリケーションに送る指示で、ユーザーからのリクエストを表します。当然、残高が10しかない口座から1000を引き出そうとする場合のように、リクエストは失敗する可能性があります。各指示は1つのmoduleに対応し、defstructを使ってフィールドを定義します。命名はMineCoinのような動詞と名詞の構造です。
defmodule MineCoin do
defstruct [
:account_id,
:nonce
]
end2. Events
EventsはCommandによって生成され、最終的に状態の変化を引き起こします。eventstoreにシリアライズして保存され、後から状態を復元する際に使用できます。命名方法はCommandとは異なり、CoinMinedのように、過去形で過去に発生した事実を表します。
defmodule CoinMined do
defstruct [
:account_id,
:nonce
]
end3. Aggregates
AggregatesはCommandを受け取って処理し、対応するイベントを生成または発生させるとともに、状態を変更するハンドラーとして機能します。
内部には2つの関数があります。executeメソッドはCommandを検証するロジックを追加するために使用し、入力は状態とcommand、成功した場合の出力はEventです。 apply関数は状態を変更するために使用します。ここで対象となるオブジェクトは、すでに生成されたeventであることに注意してください。
@spec execute(state, command)
::{ok, [event]}
| {:error, term()}
@spec apply(state, event) ::stateCommand、Event、Aggregatesが揃ったら……
次に、Commandを対応するAggregatesへ導くディスパッチ役が必要です。CommandedライブラリはRouterを提供しています:
defmodule Coins.Router do
use Commanded.Commands.Router
alias Coins.Account
alias Coins.Commands, as: C
dispatch(
[
C.MineCoin
],
to: Account
)
end最後に、Commandedが推奨するEventStoreを使用します。これはPostgreSQLをストレージエンジンとしてEventsを保存します。
これで、次のような一連のフローを構築できたことが分かります。これにより、Commandsを発行し、対応するEventsを生成できます。

最後に、データを読み取り用DataStoreへ同期するには?
ここでは、CommanedライブラリはCommanded Ecto projectionsをEvent handlerとして使用することを推奨しています。また、Kafkaを使って情報を同期することもでき、さまざまなアプリケーションシナリオに応じて適切なEvent handlerを選択できます。
ArcBlockシリーズ講座の詳細
講座に関する情報は、こちらでも随時お知らせします:https://www.arcblock.io/zh/learning/
最後に、高品質かつ高効率なチームに参加したい方は、ぜひArcBlockに加わってください!