CQRSとは?
Command Query Responsibility Segregation(コマンドクエリ責務分離)
コマンドとクエリを分離するデザインパターン
コマンド(create, update, delete)・・・システムの状態を変更するが、値を返さない
クエリ(read)・・・システムの状態は変更せずに、データの情報を取得
そもそもコマンドとクエリが分離していない状態、分離している状態とは??
コマンドとクエリが分離していない状態
・コマンドとクエリで共通のドメインモデルオブジェクトを利用している
・コマンドとクエリで共通のDBを利用している
コマンドとクエリが分離している状態(例)
・クエリの時はドメインモデルオブジェクトを介さず、必要な情報をDTO(データ転送オブジェクト)に詰め込んでいる
・コマンド用のDBとクエリ用のDBを別々に用意している
上記はあくまでCQRSの実装例。
CQRSの定義は更新と参照の責務を分離することであり、
必ずDBをわけないとだめ、DTOを使わないとだめ、ということではない。
CQRSで解決できる問題(CQRS採用のメリット)
- N+1問題
- フロントエンドとバックエンドで扱いたいデータ構造がちがうよ問題
- 「参照」と「更新」でデータの利用頻度、改修頻度が異なることによる問題
1.N+1問題
N+1問題とは?
一つのデータを取得した後、関連する情報を芋づる式に繰り返し取得(ループ処理の中で都度SQLを発行)しようとして、性能が低下してしまう問題のこと。
DBから情報を取ってきてドメインモデルオブジェクトとして再構築しようとする時によく起きる問題
→特にクエリ時にその影響が大きくなる!!
もうちょい詳しめの例
あるシステムのUserオブジェクトと、そのUserが各々所属するCompanyオブジェクトがあったとする。
また、各々のオブジェクトに対する物理テーブルが以下のようになっているとする。
Companyテーブル
id | name |
---|---|
1 | AB商事 |
2 | CDテクノロジー |
3 | EF株式会社 |
Userテーブル
id | name | company_id |
---|---|---|
1 | 田中 | 1 |
2 | 佐藤 | 1 |
3 | 鈴木 | 1 |
4 | 吉田 | 2 |
5 | 高橋 | 2 |
6 | 山田 | 3 |
7 | 渡辺 | 3 |
1つのCompanyには複数のUserが所属しており、CompanyとUserとの間には1対多の関係が成り立っている。
ここで、各Companyに所属するUser情報を参照して、出力するプログラムを作成したい。(クエリ)
↓(実行イメージ)
AB商事に所属する人は
田中
佐藤
鈴木
CDテクノロジーに所属する人は
吉田
高橋
EFテクノロジーに所属する人は
山田
渡辺
これを実現する際にドメインモデルオブジェクトを利用すると、まずはじめにcompanyテーブルからレコードを全件取得してきて各々Companyオブジェクトとして再構築。そのうえで各々のCompanyに紐づいたUserの情報をuserテーブルから取ってくる必要がある。
↓Javaで書くとこんなイメージ
//まずcompanyテーブルのレコードを全件取得
List<Company> companyList = companyRepositoy.getAll();
for(Company company: companyList){
//companyのidに一致しているuserを全件取得
List<User> userList = userRepositoy.getAll(company.getId());
System.out.println(company.getName() "に所属する人は");
for(User user: userList){
System.out.println(user.getName());
}
}
この時、実行されたSQL文は以下の4つ
SELECT * FROM company;
SELECT * FROM user WHERE company_id = 1;
SELECT * FROM user WHERE company_id = 2;
SELECT * FROM user WHERE company_id = 3;
このように、N+1問題とは、1回目のSQLでModel(今回でいうCompany)を取得し、そのModelのデータ数分(N回、今回はN=3)のSQLが実行されてしまう状態のことを言う。
※順番から考えると、N+1問題というよりは1+N問題と呼んだ方がイメージしやすい。
今回の例では3社だけなのでそれほど問題があるように見えないが、一般的なシステムのクエリでは100件以上のデータを取ってくることもざらにある。この場合100回以上SQLを実行することになり、膨大な処理時間が掛かってしまう。
N+1問題を解決するには?
→結論:クエリの時はドメインモデルオブジェクトを介さずDTOを利用する
参照側の都合に合わせたDTOを用意して、例えばテーブルをJOINしたSQLの結果をDTOに詰め込む。これを参照したい側に渡す。
↓今回の処理ではuserの名前と会社の名前さえ参照できればよいので、例えばこのようなDTOを用意。
userテーブルとcompanyテーブルをJOINしたSQLの結果をこのDTOに詰め込むだけ(SQL実行回数が1回になる!)
Javaのコードはこうなる
List<QueryDTO> dtoList = companyDAO.getAll();
for(QueryDTO dto: dtoList){
System.out.println(dto.getCompanyName() "に所属する人は");
for(String userName: dto.getUserNameList()){
System.out.println(userName);
}
}
つまりコマンドの時と、クエリの時で処理を分ける!
※コマンド時にドメインモデルオブジェクトを利用する理由については、前回の復習。
ドメインのルールをドメインモデルにまとめることでロジックの散財を防いだり、ドメインのルール的に不正な値のチェックを行うことデータの整合性を保つことができる。
ただクエリ時にはドメインモデルを使うメリットもあまりない。ので、DTOでいいじゃん!という話。
※コマンドの時はN+1問題は発生しないのか?
→全く発生しないわけではないが、影響はすくない
・基本的に処理の方向が、クエリはDBから情報もってくる側だがコマンドはDBに流す側。
・クエリの時は100件とか読み込むときあるけど、コマンドで一気に100件とかはあまりない。
→今回の例ではテーブルをJOINした結果をDTOに詰め込む例を示したが、そもそもデータの持ち方(DB)も参照用に特化したものをあらかじめ用意しておけば良いのでは??
→参照用DBの導入!
2.フロントエンドとバックエンドで扱いたいデータ構造がちがうよ問題
ドメイン駆動設計、モデル駆動開発を採用して開発しようとすると、バックエンド側としてはドメインモデルに沿った、正規化されたきれいなデータを扱いたい。
しかしフロント側、参照したい側からしたらドメインモデルなど知ったことじゃない!フロント側がみやすい形でデータくれ!
解決策
→コマンドとクエリを分離
コマンド:ドメインモデルに従い統一的に処理
クエリ:フロントエンドの都合に応じて、扱いたい形で自由に処理(ドメインモデルのことは考えない)
3.「参照」と「更新」でデータの利用頻度、改修頻度が異なることによる問題
一般的にデータの利用頻度は「参照」95%、「更新」5%だと言われている。
つまり必然的に「参照」の方が、改修の要望があがる頻度も高くなる。
しかし、参照と更新で共通の処理、共通のドメインモデルを利用していると、参照に変更を加えようとすると更新側にも影響範囲が及ぶ可能性があるため確認が発生。スピード感をもった参照側の改修ができなくなる。(「更新」の処理が足を引っ張る形に)
解決策
→コマンドとクエリを分離
あらかじめ参照と更新で処理をわけておくことで、「参照」に対する改修の要望があった時、「更新」側に影響を与えることなく、参照の方のみ改修できるようになる。
↓更新の修正は赤枠の範囲内で、参照の修正は緑枠の範囲内で閉じる。
CQRSとドメイン駆動設計の関係
上記に挙げた問題は、実はドメイン駆動設計(モデル駆動開発、オブジェクト指向といった方が正確かも)を採用することのデメリットから来ている問題でもある。
歴史的にもCQRSはドメイン駆動設計の文脈で登場したらしい。
ただ現在では、ドメイン駆動設計とは切り離して単独のパターンとして考えられることも多く、実務上ドメイン駆動設計をそこまで重要視していない設計についても、コマンドとクエリを分けるメリットはある。
CQRSとイベントソーシングの関係
CQRSを目的とした実装の一部にイベントソーシングを使うことができる。
一方、イベントソーシングを目的とした実装はほぼCQRSを併用する。
→結果的にCQRSとイベントソーシングはペアで語られることが多い
「CQRSを目的とした実装の一部にイベントソーシングを使うことができる。」の例として、コマンドDBとクエリDBの非同期連携を行うときにイベントソーシングが登場する。
イベントソーシング、イベント駆動アーキテクチャの話まで考えると、さらにCQRSのメリットが見えてくる。
次回、イベントソーシングについてやる、かも。
参考文献