little hands' lab

ドメイン駆動設計を布教したい

CQRS実践入門 [ドメイン駆動設計]

この記事はドメイン駆動設計#1 Advent Calendar 2019の2日目の記事です。

よく誤解されがちなCQRSについて解説します。

DDDの参照系処理で発生する課題

DDDで定義されている実装パターンを使っていると、基本的には永続化層との入出力はRepositoryを使うことになります。 更新系の処理ではEntityやValueObjectでドメインの知識を表現し、Repositoryを使って集約単位で永続化するという構成をとると、非常にメンテナンス性の良いものになります。

参考過去記事:
モデルでドメイン知識を表現するとは何か[DDD] - little hands' lab

一方、参照系の処理、特に一覧画面のような処理では、複数集約の値を組み合わせた結果を画面に返そうとすることが多いです。

例えば以下のようなケースを想定します。

f:id:little_hands:20191201214413p:plain:w300

タスク、ユーザー、ラベルという3つの集約があり、それぞれにRepositoryがあるとします。そのような場合に、以下のようなタスク一覧画面を参照するというのはよくあることでしょう。

f:id:little_hands:20191201214508p:plain:w400

これを1つのUseCase(ApplicationService)で実装しようとすると、3つのRepositoryからそれぞれ値を取得し、戻り値のオブジェクトに詰め替えるような実装にせざるを得ません。

途端に以下のような問題が発生します。

  • 複数の集約から値を取得して戻り値の型に詰め替える処理が、ループが増えて読みにくいコードになる
  • 画面に返す必要のない値を一度取得するのでパフォーマンスが悪化する
  • 複数集約の条件(where)で絞り込んでのページングができない
    • (今回の事例では、タスクの担当者名と、ラベル名と、タスク名で同時に検索をしてページングしようとするようなケース。ページングを1つの集約の結果だけで考えられない)

タスク一覧画面のイメージから想像していただけると思いますが、これはほぼ全てのプロジェクトで突き当たる課題だと思っています。私は実際のプロジェクトでRepositoryのみでどこまでできるか検討してみましたが、上記の課題が深刻化し、やはり対策を考えざるを得ない状況に陥りました。

解決策

CQRS(Command Query Responsibility Segregation: コマンドクエリ責務分離)というものを導入します。
CQRSとは、「情報の参照に使用するモデルと更新に使用するモデルに異なるものを使用する」というアーキテクチャです。
モデルという言葉は多義語ですが、この文脈ではアプリケーションコード上のモデル、つまり更新系のオブジェクトと参照系のオブジェクトを分けるということになります。

f:id:little_hands:20191202045502p:plain:w500

名前 具体例 UseCaseがDBアクセスする際に使用するもの
更新系モデル DDDのEntity、ValueObjectなど Repository
参照系モデル 特定のユースケースに特化した値の型。SQLの結果1レコードを1つの型にするなど (DTOといった命名をする) 専用のオブジェクト(QueryServiceといった命名をする)

なお、QueryService、DTOという名前は特に公式のものがある訳ではないので、各プロジェクトで決めていただければと思います。

この場合では、以下のようなものを参照系モデルとして定義します。

public class TaskDto {
  private String taskId;
  private String taskName;
  private String userName;
  private String labelName;
}
public interface TaskQueryService {
  public List<TaskDto> fetchByUserId(UserId userId);
}

DDDのアーキテクチャ上どのように位置付けられるかというと、QueryServiceのInterfaceと戻りの型をUseCase層に、実装クラスはRepositoryと同様にインフラ層に配置します。UseCaseからは、「このような条件を指定すると、このような型で返ってくる」というという抽象的な知識だけ持ち、実装の知識はインフラ層に隠蔽します。(IFがドメイン層ではなくUseCase層である理由は後述します)

f:id:little_hands:20191202043331p:plain:w700

そして、QueryServiceの実装クラス内では(今回の事例では)複数のテーブルをJoinして一発で取得するクエリを書き、シンプルに結果をDTOに詰め替えます。クエリ実行に使用する方法は必要に応じて選択できます。直接StringでSQLを書いても良いし、クエリビルダのようなライブラリを使用しても良いです。

なお、私がJavaのプロジェクトで実装するときは、RepositoryもQueryServiceもjOOQというタイプセーフにクエリを実行できるライブラリを使用しています。

CQRSのメリット、デメリット

これは先述の課題の裏返しになりますが、以下のようなメリットがあります。

  • 複数集約にまたがるデータを取得する際のコードがシンプルになり、メンテナンス性が高まる
  • クエリパフォーマンスが上がる、チューニングしやすくなる
  • 複数集約の条件(where)で絞り込んでのページングができるようになる

一方、デメリットは以下のようなものがあります。

  • オブジェクトの属性が参照されている場所が追いにくくなる
    • 元はGetterの参照を追えば確実だったが、別の手段を考える必要が生まれる
  • アーキテクチャ自体が複雑になり、解説が必要になる

実はメリットは非常に大きいですが、デメリットも確実にあります。
MartinFawlerの記事でも、

状況によってはCQRSは価値を発揮しますが、多くのシステムではCQRSによってリスク、複雑さが増すことに注意してください。

と述べられていました。

しかし、「課題」の最後に述べた通り、DDDを用いている限りは避けられない重要な課題を解決する手法であるため、課題の大きさ、メリットデメリットを考慮して導入判断できることが重要だと思います。

実装時の注意事項

部分的導入について

重要なことですが、CQRSは部分的な導入が可能です。
つまり、「参照用モデルと更新用モデルを完全に分ける必要はない」ということです。

どちらかというと、「必要なところだけ参照に特化したモデルを導入する」といった使い方が適切でしょう。

なぜQueryServiceの定義がUseCase層なのか

それは、QuerySerivceの戻り値がユースケースに依存したものであるからです。

今回最初に紹介した一覧画面とは別に、自分のタスクだけを表示する画面があり、そこでは以下のような表示をするかもしれません。

f:id:little_hands:20191202053446p:plain:w200

そのほかに、何らかの要件によりユーザーごとに最後に完了したタスクを表示する画面があるかもしれません。

f:id:little_hands:20191202053644p:plain:w250

これらの表示時に取得するための型は、明らかにユースケースによって異なり、それぞれで定義するべきものです。そして、「ルール・制約」をモデリング表現するドメイン層の責務には含めるべきではないでしょう。

また、これらの戻り値の型は完全に一致しない限りは使い回すべきではありません。とあるDTOには10個の項目があり、ユースケースAでは1〜5個目を、ユースケースBでは3, 4, 7〜10個目を使用する、ユースケースCでは・・・という風に最大公約数な項目を持つDTOを定義してしまうと、どこで何を使っているのかがわからなくなり、肥大化してメンテナンス性がどんどん落ちていきます。

型も、そしてインフラ層のパフォーマンスチューニングも、特定のユースケースに特化して実装するべきなのです。

整合性をどうやって担保するのか

UseCase層でテストを書きましょう。 参照処理だけのテストだけでも書くのが最低限ですが、業務上重要な部分に関しては、更新処理との結合テストを書くと良いでしょう。

例えば、承認処理をした結果が一覧に出る出ない、と言ったケースでは、承認処理のUseCaseと、参照処理のUseCaseを続けて呼び出すテストを書きましょう。 UseCaseがプレゼンテーション層の知識を持たない実装になっていれば、続けて呼び出すことは比較的容易になっているはずです。

よくある誤解

データソースを分ける必要があるのか

データソースを分離するアーキテクチャは、このようなものです。

f:id:little_hands:20191202055456p:plain:w500

「CQRS = データソース分離」と思われることがありますが、これは誤解です。

データソース分離は、別の課題にあわせて、モデルを分離した次のステップと考えることができます。想定される課題は、参照系のパフォーマンス問題です。

一般に、参照系のリクエスト数は更新系のリクエスト数より圧倒的に多いです。そこで、参照系のパフォーマンスを上げたい時、更新系とはデータソースを分離することで、参照系のインスタンスだけスケールアウトするなどのパフォーマンスチューニングが可能になります。 また、インスタンス単位で分けなくても、参照系処理向けにマテリアルビューを作成する、というものデータソース分離の一環として考えることができるでしょう。

データソース分離が解決する課題はモデルの分離とは別のもの なので、きちんと課題と解決策が対応しているかを判断して、導入を判断する必要があります

イベントソーシングとの関係

「CQRS = イベントソーシング」も誤解です。

こちらもCQRS文脈でよく一緒に出てくるので混乱しやすいですが、「CQRSとイベントソーシングは相性が良い」というだけであり、その二つは別で考えることができます。

イベントソーシングとは、データ永続化をドメインオブジェクト(EntityやValueObject)の状態(ステート)をそのまま保存するのではなく、「ユーザーが登録された」「タスクが完了された」といった イベントそのものを永続化する というアーキテクチャです。

参照時にすぐクエリできるように集計したデータを永続化する、と言った処理を挟むことがあり、その中で、必然的に参照/更新のモデルの分離や、データソースの分離が行われます。

つまりイベントソーシングはCQRS、データソース分離を含みますが、CQRSがイベントソーシングを含んでいる訳ではないのです。

なお、イベントソーシングが解決したい課題は、これまでに述べられた参照更新のパフォーマンス最適化以外に、積み上げ型データになることにより証跡を残せることなどが上げられますが、こちらについての詳細は別の機会としたいと思います。

過去資料との繋がり

ちょうど2年前にCQRSとORMについてJJUGで発表しました。

www.slideshare.net

CQRSというものについてのスタンスは、このブログ記事の内容とスライド資料の内容は基本的にな同じですが、ORMについては2年前と今では違うスタンスです。

スライド資料では更新系はSpringData + Hibernate、参照系はjOOQという組み合わせで発表していましたが、現在では更新系も参照系もjOOQ一択です。

SpringDataはドキュメントにもDDDへの言及がある通り、かなりDDDを意識したライブラリではあるのですが、結局アノテーションでテーブルの情報がEntityに埋め込まれてしまったり、基本的にはテーブルとオブジェクトの形は対応していなければいけなかったりと、インフラ層の知識によって制限が出てしまいました。
当時は「ドメイン層もアノテーションだけはギリOK」としていましたが、結局アノテーションを通じてデータベースへの依存が発生していたのです。

よって、現在ではRepositoryの中でjOOQを使って詰め替えを行う形が一押しです。

まとめ

CQRSは若干複雑であり、誤解や混乱が多いトピックですが、DDDで実装する上で避けられない課題を解決できる、非常に強力な手法です。

是非とも課題、メリット、デメリットを正しく理解し、使用を検討してみてください。