インドカレーファンクラブ

パソコン、カメラ

CQRS提唱者のドキュメントを読む(イベント部分抜きで途中まで)

読まないのもよくないかな?と思って読んだ
でまとめた

こちらを参考にしながら
https://sipadan2003.blogspot.com/2013/12/cqrs.html

この記事(僕の書いた記事)で引用している訳文はDeepLと僕の共作

(僕の)まえがき

DDDの基本的な話を理解していないと分からないと思う

読んだら実装例とかレイヤ分割の話とか載っているかな?と思ったけどそんなことなくてショックだった
しかも分かりにくいし!

先にこちらを読んでおくと理解が速い
寧ろこっちにはある程度の実装例書いてくれているし大変ありがたい(実践の名に即している!)

little-hands.hatenablog.com

一応原典を読むことで理解は深まりはした

A Stereotypical Architecture

この節ではDDD(ステレオタイプアーキテクチャ)の問題点の指摘を行っている

ステレオタイプアーキテクチャってこんな感じ

f:id:omdwn:20210709221155j:plain
ステレオタイプアーキテクチャ

図:僕作

それらのアーキテクチャの有する動詞はCRUDの4つのみ
※ DDDというかそのDomainObjectを対象にRepositoryで行う操作はCRUDに集約してしまっているということ
しかし現実問題のユースケースCRUDで十分ということは滅多に無い
例えば後述するような「販売を完了する」、「発注書を承認する」、「ローン申請を提出する」など
上の図では早速全てを無視してCreateじゃなくてSaveとか書いてるし他のメソッドを端折ってるけど気にしないで

更に言うと、DBが原因でボトルネックを抱えがち(かつ、スケールさせにくい)

ということで本書(CQRS Documents by Greg Young)ではこれらの課題に対してアプローチをかけていく
その柱となるのがCQRS

Task Based User Interface

Instead of simply sending the same DTO back up when the user is completed with their action the client needs to send a message to the Application Server telling it to do something. It could be to “Complete a Sale”, “Approve a Purchase Order”, “Submit a Loan Application”. Said simply the client needs to send a message to the Application Server to have it complete the task that the user would like to complete. By telling the Application Server what the user would like to do, it is possible to know the intention of the user.
ユーザーのアクションが完了したら、同じDTOを単純に送り返すのではなく、クライアントはアプリケーション・サーバに何かをするようにメッセージを送る必要があります。例えば、「販売を完了する」、「発注書を承認する」、「ローン申請を提出する」などです。簡単に言えば、クライアントはアプリケーション・サーバーにメッセージを送り、ユーザーが完了させたいタスクを完了させる必要があるのです。ユーザーが何をしたいのかを伝えることで、アプリケーション・サーバはユーザーの意図を知ることができます。

課題の残るDDDのアーキテクチャ的にはクライアント(Presentation)がアプリケーション(ApplicationService)にメッセージ(引数)として渡すのはシンプルなDTOで、それそのものではなにしてるのか分かりにくい

でも、ドメインオブジェクトをコピーしただけのDTOではなく、具体的な処理に合わせたオブジェクトがメッセージとして送られたら何を意図しているのか分かりやすいよね?

Commands

そこで上述したメッセージ(引数)に以下のようなCommandObjectを利用しようという話

例えばこれは商品の非活性化処理用のコマンド(引用)

public class DeactivateInventoryItemCommand {
    public readonly Guid InventoryItemId;
    public readononly string Comment;

    public DeactivateInventoryItemCommand(Guid id, string comment) {
        InventoryItemId = id;
        Comment = comment;
    }

これはボトムアップDDDでも出てきたものなので特に細かく書く必要もないかなぁ
分かりにくかったら次の話まで読み進めたほうが分かりやすいかも

User Interface

こっちはボトムアップDDDでは見なかった話

In order to build up Commands the User Interface will generally work a bit differently than in a DTO up/down system. Because the UI must build Command objects it needs to be designed in such a way that the user intent can be derived from the actions of the user.
コマンドを構築するために、ユーザーインターフェースDTOアップ/ダウンシステム(訳注:DDDのApplicationServiceのI/OをDTOにすることっぽい)とは少し違った動作をします。UIはコマンドオブジェクトを構築する必要があるため、ユーザーのアクションからユーザーの意図を汲み取ることができるように設計する必要があります。
The way to solve this is to lean more towards a “Task Based User Interface” also known as an “Inductive User Interface” in the Microsoft world.
この問題を解決する方法は、「タスクベースのユーザーインターフェイス」(Microsoftでは「インダクティブ・ユーザーインターフェイス」と呼ばれています)に近づけることです。

つまり、タスクベースUIという考え方に準じた設計をすることでユーザにとって使いやすいシステム、及び前述のCommandパターンが実現できるようになる

例をあげて説明すると...

(課題の残るDDDのアーキテクチャでは) 例えば商品を表示/非表示するという処理を実現する際に、その引数にDTO、つまりDomainObjectたるItemをコピーしたInventoryItemDTOを利用する
その場合のUIは以下のようなイメージになる

--------------------------------------
商品ID: xxx
    名称    :     [   ]
    提供者  :     [   ]
    仕入れ値:     [   ]
    数量    :     [   ]
    ステータス:   [   ] (表示 or 非表示)
    非表示理由:   [   ] (ステータスで非表示を選んだ時だけ表示)

[ 送信 ]
--------------------------------------

POSTされるであろうこれらのInputに対応するため、InventoryItemDTOにはフィールドが多くなるのは必然だ

一方、タスクベースUIでは、アイテムのリストがまず存在していて、その個々のアイテムに対して非活性化するためのリンクやボタンを用意する
そのリンクを押下した際に以下のようなUIがポップアップなどで表示される

--------------------------------------
商品ID | 商品名 | ステータス |
000001 | ミカン | 表示中     | [非表示]
000002 | トマト | 表示中     | [非表示]
000003 | サンマ | 非表示     |
--------------------------------------

↓ ミカンの非表示リンクを押下

--------------------------------------
商品ID: 000001
商品名: ミカン

商品を非表示とする場合はその理由を入力する必要があります
[                            ] [ 送信 ]
--------------------------------------

タスクベースUIにおいてCommandパターンを採用する場合、これらのInputを受けるのは、CommnadObjectとすることが出来る
例えば今回の場合、そのオブジェクト名は上述したような DeactivateInventoryItemCommand あたりになるだろう(非表示と非活性と命名が微妙だけど)
そしてそのオブジェクトが持つべきフィールドは商品ID、コメント(非表示の理由)のみである
上述の通りのItemDTOを利用する場合よりシンプルかつDTOの役割が明確化される

Command and Query Responsibility Segregation

CQRS コマンドクエリ債務分割

Origins

本ドキュメントではCQSの定義についてはWikipediaのそれを参照している

It states that every method should either be a command that performs an action, or a query that returns data to the caller, but not both. In other words, asking a question should not change the answer. (Wikipedia)
つまり、すべてのメソッドは、「アクションを実行するコマンド」「呼び出し元にデータを返すクエリ」のいずれかでなければならず、両方であってはならない。言い換えれば、質問をしても答えを変えてはいけないということです。

それに対しMartin Fowlerからの問題提起がある

Meyer likes to use command-query separation absolutely, but there are exceptions. Popping a stack is a good example of a query that modifies state.
Meyerはコマンドとクエリの分離を絶対に使いたがりますが、例外もあります。スタックのポップは、状態を変更するクエリの良い例です。
Meyer correctly says that you can avoid having this method, but it is a useful idiom. So I prefer to follow this principle when I can, but I'm prepared to break it to get my pop.
Meyerは正しく、このメソッドを利用しないようにすることができると言っていますが、ポップは便利なイディオムです。ですから、私はできる限りこの原則に従いたいと思っていますが、ポップを利用するためにはそれ(訳注: コマンド/クエリの分離)を破ることも覚悟しています。
https://martinfowler.com/bliki/CommandQuerySeparation.html

スタックのポップは言うなればスタックの一番上の値を取得し、スタックからそれを削除するようなもの
それは、Javaイテレータの反復処理Nextメソッド(カーソルを次に進め、その要素を返す)も似たようなもので、コマンドとクエリが同時に行われてしまっていることに他ならない

CQRSは当初こうしたCQSの拡張だと思われていたけど、実際は別のものですよ。ということらしい

参考:https://qiita.com/hirodragon/items/6281df80661401f48731

Command and Query Responsibility Segregation uses the same definition of Commands and Queries that Meyer used and maintains the viewpoint that they should be pure. The fundamental difference is that in CQRS objects are split into two objects, one containing the Commands one containing the Queries.
コマンドとクエリの責任の分離では、Meyerが使用していたコマンドとクエリの定義をそのまま使用し、それらが純粋であるべきだという視点を維持しています。基本的な違いは、CQRSではオブジェクトが2つのオブジェクトに分割されることです。1つは複数のコマンドを含むオブジェクト、もう1つは複数のクエリを含むオブジェクトです。

CQRSを適用していないサービス
サービスがコマンドとクエリの両方を担っている
ドメインオブジェクトはその両方で利用される

CustomerService
void MakeCustomerPreferred(CustomerId)
Customer GetCustomer(CustomerId)
CustomerSet GetCustomersWithName(Name)
CustomerSet GetPreferredCustomers()
void ChangeCustomerLocale(CustomerId, NewLocale)
void CreateCustomer(Customer)
void EditCustomerDetails(CustomerDetails)

CQRSを適用したサービス
Writeがコマンド側、Readがクエリ側となる

CustomerWriteService
void MakeCustomerPreferred(CustomerId)
void ChangeCustomerLocale(CustomerId, NewLocale)
void CreateCustomer(Customer)
void EditCustomerDetails(CustomerDetails)

CustomerReadService
Customer GetCustomer(CustomerId)
CustomerSet GetCustomersWithName(Name)
CustomerSet GetPreferredCustomers()

そして、コマンド・クエリのそれぞれについて、アーキテクチャの特性は大きく異なる傾向にあるとのことで、いくつかの側面から見た例が挙げられている

それらを引用すると↓

Consistency

Command: It is far easier to process transactions with consistent data than to handle all of the edge cases that eventual consistency can bring into play.
Query: Most systems can be eventually consistent on the Query side.
コマンド: 最終的な一貫性がもたらすエッジケースをすべて処理するよりも、一貫性のあるデータでトランザクションを処理する方がはるかに簡単です。
クエリ: ほとんどのシステムはクエリ側で最終的に一貫性を保つことができます。

コマンド側の「よりも」というのはCQRSを適用していない場合のことを言っているのだろう
分かりにくいけど何となく理解できる

Data Storage

Command: The Command side being a transaction processor in a relational structure would want to store data in a normalized way, probably near 3rd Normal Form (3NF)
Query: The Query side would want data in a denormalized way to minimize the number of joins needed to get a given set of data. In a relational structure likely in 1st Normal Form (1NF) コマンド: コマンド側はリレーショナル構造のトランザクションプロセッサであるため、データを正規化して保存したいと考えています(おそらく第3正規形(3NF)に近い形で)
クエリ: クエリ側では、あるデータセットを得るために必要な結合数を最小限にするために、データを非正規化して保存します。リレーショナル構造の場合、おそらく第1正規形(1NF)になります。

コマンド側では正規化されたオブジェクトを利用しDB更新処理を行う
つまり、テーブルと1:1で関連付けるような、厳しい縛りのORMで使うオブジェクトを使うイメージ(RailsのModelみたいなもの)

クエリ側ではそのクエリのSELECTする内容ごとに適した結果がほしいので、非正規化されたオブジェクト、つまりクエリのユースケースごとに適したフィールドを有するオブジェクトをDBから読み込んだデータにマッピングする

Scalability

Command: In most systems, especially web systems, the Command side generally processes a very small number of transactions as a percentage of the whole. Scalability therefore is not always important.
Query: In most systems, especially web systems, the Query side generally processes a very large number of transactions as a percentage of the whole (often times 2 or more orders of magnitude). Scalabilityis most often needed for the query side.
コマンド: ほとんどのシステム、特にウェブシステムでは、コマンド側で処理するトランザクションの数は、全体に占める割合としては非常に少ないのが一般的です。そのため、スケーラビリティは必ずしも重要ではありません。
クエリ: ほとんどのシステム、特にウェブシステムでは、クエリ側は一般的に全体の中で非常に多くのトランザクションを処理します(しばしば2桁以上)。スケーラビリティが最も必要とされるのは、クエリ側です。

これは分かりやすい。書いてあるとおり

The Query Side

クエリ側はデータを取得するメソッドのみを持つ

クライアントに返すDTOsにはドメインオブジェクトを利用せず、クライアントの要求を満足するDTOをそれぞれ生成し利用する

これによってCQRS非導入時のステレオタイプアーキテクチャに起こりがちな以下の問題を回避できる
この辺はCQRSについて説明した文章でよく見る内容だと思う

  • リポジトリの大量の読み取りメソッドには、ページングやソートの情報も含まれることがある
    • ページングやソートの情報はドメインの情報とは無関係
  • DTOを構築するために利用されるドメインオブジェクトのgetterが予期せぬ利用を生む危険がある
  • クエリで絞り込まない場合、データをメモリに展開してからの絞りこみになりメモリ使用量が増える
  • DTO を構築するために複数の集約ルートを無駄に読み込むことになる
    • SELECT句で利用しないカラムまでメモリに読み込むことになる
    • これによって集約の境界が混乱することがある
  • 何よりクエリの最適化が出来ない (インピーダンスミスマッチ)

(図は原典のを見てください)

クエリ側ではドメインを利用することなく、Thin Read Layerによってデータベースから直接読み込みDTOsにマッピングを行う

The Command Side

(図は原典のを見てください)

コマンド側がクエリ側と違い比較的ステレオタイプアーキテクチャに似た形のまま

クエリ側で記述した「CQRS非導入時のステレオタイプアーキテクチャに起こりがちな問題」はコマンド側にも恩恵をもたらす
ドメインオブジェクトはフィールドのアクセサを広くとる必要がなくなり、Repositoryでは GetById を除くとクエリ用メソッドは僅かしか残らない。 また、集約の境界について今まで以上にふるまいに焦点を絞ることができる

こうしてコマンドとクエリを分離すると、それぞれで読むデータソースを別にすることで、タスクに合わせてデータモデル最適化できる
例えば、上述したようにクエリ側は非正規化状態(第1正規形)のデータ、トランザクショナルモデル(コマンド側?)は正規化状態のデータ(第3正規形)というような形

個人的には、クエリ側はバッチで生成したサマリテーブルでも読むようにすると楽になると思う

続き

Events as a Storage Mechanism から先は読んでないけど気が向いたら...

(僕の)感想

これ結局実践CQRS入門読んだ方がいいわ

あとMartin Fowlerのも読まなきゃ

martinfowler.com