メインコンテンツへスキップ

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

周蕾 (ArcBlock 后端工程师)
ArcBlockcqrs

著者 周蕾(ArcBlockバックエンドエンジニア)

ここ数年、DDDの分野ではCQRSアーキテクチャという概念をよく目にします。CQRSはコマンド・クエリ責務分離パターン(Command Query Responsibility Segregation)の略称です。ちょうど最近、ArcBlockのバックエンドサービスでCQRSアーキテクチャの採用を検討しているため、今回は最近の調査で得た知見を皆さんと共有します。本稿ではEvent Sourcingから出発してCQRSを紹介し、Commanded(Elixirのライブラリ)を通じて、ES/CQRSの概念に従ってアプリケーションを開発する方法を見ていきます。

Event Sourcing(イベントソーシング)とは?

Event Sourcingの本質は、現在の物事の状態ではなく、発生したイベントそのものを保存することです。Event Sourcingの概念では、Eventはすでに発生した確定済みの事実であり、最小単位でもあります。 たとえば:

図1

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

図1

次のようにまとめられます:

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)を分けます。これは、照会と更新の過程で使用するデータモデルも異なることを意味します。こうして読み取りと書き込みのロジックが分離されます。

図1

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
        ]
    end

2. Events

EventsはCommandによって生成され、最終的に状態の変化を引き起こします。eventstoreにシリアライズして保存され、後から状態を復元する際に使用できます。命名方法はCommandとは異なり、CoinMinedのように、過去形で過去に発生した事実を表します。

    defmodule CoinMined do
        defstruct [
            :account_id,
            :nonce
        ]
    end

3. Aggregates

AggregatesはCommandを受け取って処理し、対応するイベントを生成または発生させるとともに、状態を変更するハンドラーとして機能します。

内部には2つの関数があります。executeメソッドはCommandを検証するロジックを追加するために使用し、入力は状態とcommand、成功した場合の出力はEventです。 apply関数は状態を変更するために使用します。ここで対象となるオブジェクトは、すでに生成されたeventであることに注意してください。

  @spec execute(state, command)
      ::{ok, [event]}
      | {:error, term()}

  @spec apply(state, event) ::state

Command、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を生成できます。

図1

最後に、データを読み取り用DataStoreへ同期するには?

ここでは、CommanedライブラリはCommanded Ecto projectionsをEvent handlerとして使用することを推奨しています。また、Kafkaを使って情報を同期することもでき、さまざまなアプリケーションシナリオに応じて適切なEvent handlerを選択できます。

ArcBlockシリーズ講座の詳細

講座に関する情報は、こちらでも随時お知らせします:https://www.arcblock.io/zh/learning/

最後に、高品質かつ高効率なチームに参加したい方は、ぜひArcBlockに加わってください!