こんにちは、農業系プロダクトの開発を担当している糸井です。
弊社ではピンポイントタイム散布(以下、PTS)という防除のデジタル化サービスを展開しております。
PTSシステムのバックエンドではSpringBootを使用しております。
立ち上げ期より、既存の農業系プロダクトの課題感をもとに改善を進めてきましたので、その軌跡をご紹介します。
前回までの記事はこちら
- ピンポイントタイム散布(PTS)でのバックエンド改善の軌跡 ~1. OnionArchitecture~ - OPTiM TECH BLOG
- ピンポイントタイム散布(PTS)でのバックエンド改善の軌跡 ~2. DDD, OnionArchitecture~ - OPTiM TECH BLOG
- ピンポイントタイム散布(PTS)でのバックエンド改善の軌跡 ~3. PublicSetter~ - OPTiM TECH BLOG
- ピンポイントタイム散布(PTS)でのバックエンド改善の軌跡 ~4. Repository~ - OPTiM TECH BLOG
前回の記事では、Repositoryに対する事例をご紹介しました。
今回はCQRSについてご紹介します。
5. CQRSの導入
前回の記事 4-1. Repository の引数と戻り値の型を統一する
, 4-2. RepositoryImpl の導入
を行うと、以下のような場面が出てきます
- RepositoryImplの中で複数のテーブルに別々のQueryを実行する
- ループの中でQueryを実行する
結果として以下のような問題が発生します。
- API Responseを返すのが遅くなる
- DatabaseとのConnectionが枯渇しやすくなる
上記のような問題を解決するためにCQRSというパターンがあります。
PTSでもCQRSパターンを取り入れたので、その構成や背景、導入結果について説明していきます。
より詳細なCQRSパターンの解説は以下の記事をご参照ください。
- AWS > ドキュメント > AWS Prescriptive Guidance> マイクロサービスにおけるデータ永続化の有効化 > CQRS パターン
- Learn > Azure > アーキテクチャ センター > CQRS パターン
- little hands' lab > CQRS実践入門 [ドメイン駆動設計]
CQRSパターンを取り入れたPackage構成
特定のUsecaseでしか使用しないことが分かっているので、 関連する実装はUsecaseのPackageの中にまとめています。
app
└── src
├── main
│ ├── java/...
│ │ ├── domain
│ │ │ ├── entity
│ │ │ ├── value_object
│ │ │ └── repository
│ │ ├── presentation
│ │ │ └── controller
│ │ └── usecase
│ │ ├── query_service
│ │ │ ├── XxxYyyMapper.java ← このUsecase専用のSQLのためのInterface
│ │ │ └── XxxYyy.java ← このUsecase専用のMapperのための引数や戻り値のClass
│ │ ├── XxxYyyUsecase.java
│ │ ├── XxxYyyParam.java
│ │ └── XxxYyyDto.java
│ └── resources
│ ├── domain
│ │ └── repository
│ └── usecase/query_service
│ └── XxxYyyMapper.xml ← このUsecase専用のQuery
└── test
PTSでCQRSパターンを取り入れた背景は2つあります。
PTSでCQRSパターンを取り入れた背景1つ目
4-1. Repository の引数と戻り値の型を統一する
では、
Domain Modelとは全く関係なく、実質DTO状態になっていたEntityや、
そのEntityを引数・戻り値に使っていたSQLの置き場所をどうしたかについて触れてませんでした。
可能な限りDomain Modelに従ったEntityに統合しましたが、
統合が難しかったClassについては特定のUseCaseでしか使わないので、Query Service
というPackageを切って、
そのUseCase専用のSQL、Classとして扱うようにしました。
PTSでCQRSパターンを取り入れた背景2つ目
こちらは最初に記載した2つの問題点を解決するという素直な目的から来ています。
複数の集約Rootを跨いだダッシュボード的な画面表示のためにこんなQueryを実行することがあります。
SELECT table_a.1 , table_a.2 , table_b.3 AS aaa , table_b.4 AS bbb , table_a.5 , table_a.6 , table_b.7 , exists ( SELECT 1 FROM table_f WHERE 8 = table_a.9) AS ccc , table_g.10 , table_g.11 , (IFNULL(table_g.12, 1) + IFNULL(table_h.13, 0)) AS ddd , table_a.14 , table_a.15 , table_a.16 FROM table_a LEFT JOIN table_b ON table_b.17 = table_a.18 LEFT JOIN ( SELECT table_c.19 , SUM(table_c.20) AS eee , SUM(table_g.21) AS fff , SUM(table_g.22) AS ggg FROM table_c LEFT JOIN ( SELECT table_d.23 , COUNT(table_e.24) AS hhh , COUNT(table_e.25 = 2 OR table_e.26 = 3 OR NULL) AS iii FROM table_d LEFT JOIN table_e ON table_e.27 = table_d.28 GROUP BY table_d.29 ) table_g ON table_g.30 = table_c.31 GROUP BY table_c.32 ) table_g ON table_g.33 = table_a.34 LEFT JOIN ( SELECT table_i.35 , COUNT(DISTINCT table_i.36) AS jjj FROM table_i WHERE table_i.37 NOT IN ( SELECT table_e.38 FROM table_e LEFT JOIN table_d ON table_d.39 = table_e.40 LEFT JOIN table_c ON table_c.41 = table_d.42 WHERE (table_e.43 = 2 OR table_e.44 = 3) AND table_c.45 = table_i.46 ) GROUP BY table_i.47 ) table_h ON table_h.48 = table_a.49 WHERE table_a.50 IN (kkk, ...) ORDER BY table_a.51 DESC
SUM
, COUNT
が3つずつあったり、Sub Query
の Sub Query
の中で LEFT JOIN
していたりと、
Domain Model に対して RepositoryImpl で使う Query には見えないですね。
このように通常のRepositoryでは扱いきれないQueryに対して、CQRSパターンを導入しています。
CQRSを取り入れた結果
4-1. Repository の引数と戻り値の型を統一する
, 4-2. RepositoryImpl の導入
でも触れましたが、
Repositoryの引数と戻り値は集約RootのEntityのままにし、同じ spec (仕様) を守れるようになりました。
似たような実質DTO状態のEntityがポコポコ増えていくということも無くなりました。
つづきはこちら (2025/4/04 追記)
ピンポイントタイム散布(PTS)でのバックエンド改善の軌跡 ~6. メンバー変数への再代入禁止 ~ - OPTiM TECH BLOG
最後に
OPTiMではチームで協力し、難しく大きな課題を楽しみながらチャレンジしていきたいというメンバーを大募集しております。 ご興味がありましたら、下記フォームよりご応募ください。