はじめに
前回のpart4.2では、ヘキサゴナルアーキテクチャについて少し深掘りの説明をしました。さて、今回はCQRSについて解説します。なお、今回は前回と異なり1つの記事で完結します。
では、始めていきます!
CQRSとは
そもそも、CQRSは「Command Query Responsibility Segregation」の略で、日本語ではほぼ訳語のままに、「コマンドクエリ責務分離」と呼ばれます。その名の通りcommandとクエリを分離することで、更新と取得それぞれに特化したモデルと処理を実装します。
コマンドクエリ責務分離とは
CQRSパターンのもとになっているものは「CQS(Command-Query Separation、コマンドクエリ分離原則)」です。これはBertrand Meyer氏が考えたもので、「オブジェクト指向入門」に記載されています。
根本的な考え方は、オブジェクトとメソッドを明確に2つのカテゴリ(コマンド、クエリ)に分類しようというものです。一度ここで「コマンド」「クエリ」について整理しましょう。
- コマンド(Write)
- 一言で表すとシステムデータを「変える」処理。
具体的には、データベースの情報を書き換える、または更新する操作。このときルールとして「値を返してはいけない(戻り値はVoid型である)」という制約がある。 - クエリ(Read)
- 一言で表すとシステムデータを「見る」処理。
具体的には、データベースから情報を読み取る操作。このときルールとして「副作用(データの変更)があってはならない」という制約がある。
DDDにおけるCQRSでの流れ
では、下の画像に従って、CQRSの流れを見ていきましょう。(①~⑦)
- 1,コマンド処理(コマンドプロセッサ/コマンドハンドラ)
- サーバー側で更新処理を開始します。この処理部分はコマンドプロセッサ(コマンドハンドラ)と呼ばれます。構築方式としては、「分類方式(アプリケーションサービスに複数のコマンドメソッドを追加)」、「専用方式(単一メソッドをもつ単一クラスを作成)」、「メッセージング方式」の3つがあります。
まずは、「分類方式」と「専用方式」についてです。この2つは同期処理の場合にどちらを採用するか選択することができます。 前者のメリットは開発が容易であること、後者のメリットはコマンドごとの責務が明確になることです。
次に、「メッセージング方式」についてです。これは非同期処理の場合に用います。この方式では「専用方式」のコマンドクラスを非同期のメッセージングとして送信します。複雑になるため、スケーラビリティ(使用者数、処理数の増加に柔軟に対応する能力)が必要な場合のみ選択するようにします。同期処理と非同期処理の違いについて
同期処理
サーバーでの処理が完了するまで待つ方法。確実性が高い反面、待機時間がながくなってしまうという欠点がある。
非同期処理
サーバーでの処理が完了するのを待たずに次に進める方法。待機時間を無視できるが、処理の結果がわからないまま進めるため、エラーが発生したときに確認が遅れる可能性がある。どの方式を用いるにせよ、コマンドプロセッサは、コマンドモデルである集約のインスタンスを作成、取得して更新用のメソッドを実行します。集約については後のpartで解説しますが、今のところは「オブジェクトのまとまり」のような認識で大丈夫です。
- 2,コマンドモデル(Writeモデル)
- 通常、集約がコマンドモデルとなります。コマンドモデルの更新系メソッドが呼び出される場合、最後に「ドメインイベント」が発行されます。具体的には「会員登録が完了したとき」というようなイベントを発行します。
- 3,コマンド用データストア
- コマンドモデルの更新結果が保存されます。データの更新内容がリポジトリ経由で格納されます。
- 4,クエリモデル(ビュー)を更新(イベントのサブスクライバ)
- サブスクライバ(購読者)が発行されたドメインイベントを受信します。そして、受け取ったイベントの内容に従ってクエリモデル(クエリ用のデータ)を更新します。
- 5,クエリモデル用データストア
- ここには描画用のデータが格納されています。格納場所の決まりはありませんが、一般的にはデータベースのテーブルです。高速化のために事前に(複数の)テーブルをジョインして非正規化した(あえてデータの重複や冗長性を持たせて、データ取得のパフォーマンスを上げた)「マテリアライズドビュー」を使用することもあります。それを使うときはデータベースの標準機能を使用する場合やプログラムで事前にデータを構築する場合があります。また、性能の観点から複数台のレプリカを持つこともあります。
「事前に(複数の)テーブルをジョインして非正規化した」とは
「ジョイン」とは「結合」のことで、複数のテーブルを1つのテーブルにまとめること。「非正規化」とはあえてデータの重複や冗長性をもたせることで、データ取得のパフォーマンスを上げること。
デメリットとしては、データの整合性の管理が難しくなることが挙げられ、ある1か所が変わるとそれに関連するすべてのテーブルで更新が必要になる場合がある。 - 6,クエリモデル(Readモデル)
- 画面表示や印刷用のために非正規化されたデータモデルです。
- 7,クエリ処理(プロセッサ)
- データベースの結果セットをそのままかJSON/XMLで戻したり、DTO(データトランスファーオブジェクト)という描画モデルに詰め替えたりします。これらの方式については、プロジェクトにおいて最適な方法を選択します。
CQRSでの同期、非同期の採用指針
先ほどの「コマンド処理(コマンドプロセッサ/コマンドハンドラ)」で紹介した、同期的に処理するか、非同期的に処理するかは機能要件に左右されます。
同じデータベース内に「コマンドモデル用データストア」と「クエリモデル用データストア」を用意し、これらのデータ更新を同一のトランザクションを用いれば(まとめて処理するようにすれば)同期的に処理することができます。
#CQRSと結果整合性
「結果整合性」とは、「結果として一貫性が保たれていることが保証されていれば問題ない」という考え方です。標準的なRDBMS(リレーショナルデータベース、別記事で紹介するかも)アプリケーションでは、1つの処理でビジネスルールを正しく保つ「トランザクション整合性」が一般的です。しかし、DDDではドメインエキスパートの観点から、最終的に一貫性が保たれれば多少のタイムラグが合っても問題がないと判断される場合、「結果整合性」を用いることで複雑性を排除することができます。
おわりに
今回の記事では、CQRSについて紹介しました。次回は「イベント駆動ア^期テクチャ」について紹介します。
では、最後までお読みいただきありがとうございました。次回もよろしくお願いします。