TT Lab
はじめる
学ぶ 学習パス コース

Spring Boot — クエリが何本出るか数える

一覧一つにクエリ301本

TT Labで続きを見る

目標

Springが代わりにやってくれることを見ていきます。検証・例外の変換・トランザクション境界・遅延ロードはすべて目に見えない場所で起こるため、問題も目に見えません。

特に最後の1つ、つまり一覧を1つ表示するのにクエリが何本出るかを実際に数えます。

環境

Java 21・Maven・Spring Boot 3.4.1・H2(メモリ)。ネットワークはありません。

mkdir -p /root/work/spring
cp -r /opt/lab/samples/spring-starter/* /root/work/spring/
cd /root/work/spring
mvn -o -B test          # -o 를 빠뜨리면 원격을 보러 가서 실패합니다

依存関係は、スケルトンのpom.xmlにあるものだけが使えます(web、validation、data-jpa、actuator、h2、test)。新しい依存関係はダウンロードできません。

契約

採点ツールはクラスを参照せず、HTTPとクエリ数だけで判定します。クラスをどう分けても、以下を満たせば問題ありません。

パス 契約
GET /healthz {"status":"ok"}
POST /items {name, price}・正常は200/201・違反は400
GET /items 保存された一覧(JSON配列)
GET /items/{id} あれば200・なければ404とメッセージ
POST /items/bulk 配列・1件でも失敗したらすべてロールバック
GET /shops 店ごとの品物数・クエリは2本以下

パッケージはlabhubにしてください。

ステップ

  1. スケルトン・/healthz
  2. @Valid → 400
  3. @RestControllerAdvice → 404
  4. コンストラクター注入
  5. @Transactionalのロールバック
  6. N+1を数えて減らす → 06-queries.txt
  7. @WebMvcTestのスライス
  8. まとめ → 08-notes.md

参考

採点ツールは、プロジェクトをコピーに移して検査します。srcに採点用のファイルが残ることはありません。

プロジェクトを立ち上げる

/root/work/springにSpring Bootプロジェクトを作成し、GET /healthzが{"status":"ok"}を返すようにしてください。パッケージはlabhubです。

スケルトンが用意されています: mkdir -p /root/work/spring && cp -r /opt/lab/samples/spring-starter/* /root/work/spring/。ネットワークがないため、常に-o(オフライン)を付けます: mvn -o -B test。スケルトンのpomにない依存関係はダウンロードできません。

検証をフレームワークに任せる

POST /itemsを作成し、リクエスト本文を@Validで検証してください。名前が空、または価格が負の値なら400、正常なら200/201を返す必要があります。

record ItemRequest(@NotBlank String name, @Min(1) int price)を使います。@Validを付けないと、制約アノテーションは何もしません。付けてあるのに検証が働かないケースの大半がこれです。if (name == null)と書いているなら、フレームワークの仕事を奪っています。

例外を1か所でステータスコードに変換する

GET /items/{id}で存在しないidには404とメッセージを返すようにしてください。処理は@RestControllerAdviceの1か所にまとめます。

コントローラーごとにtry-catchを散らすと、どの例外がどのコードで返るのか、誰にもわからなくなります。return ResponseEntity.ok(Map.of("error", ...))で200を返してはいけません。ステータスコードは契約であり、プロキシやモニタリングがそれを見て動作します。

コンストラクター注入に変える

@Service層を置き、フィールドに@Autowiredを付けないでください。コンストラクター注入だけを使います(private final)。

コンストラクターが1つなら、@Autowired自体が不要です。得られるもの: finalにできる、テストでそのまま渡して差し替えられる、依存関係がいくつあるか目に見える、そして循環参照が起動時に失敗することです。運用中に失敗するよりましです。

一括処理が失敗したらすべて元に戻す

POST /items/bulkで配列を受け取って保存してください。1件でも不正ならすべてロールバックされ、すべて正常ならすべて保存される必要があります。

@Transactionalをサービスメソッドに付けます。注意点は3つあります。チェック例外はデフォルトではロールバックされず(rollbackForが必要)、同じクラスの中からthis.method()で呼び出すとプロキシを経由しないため無視され、privateメソッドには付きません。

クエリを数えて減らす

店と品物を1:Nの関係にして、GET /shopsが各店の品物数も返すようにしてください。クエリ数を計測し、修正前と修正後を06-queries.txtに書いて、最終的に2本以下にしてください。

application.propertiesにspring.jpa.properties.hibernate.generate_statistics=trueを入れ、テストでStatistics.getPrepareStatementCount()を使って数えます。まずfindAll()で実装して、店の数だけクエリが余分に出ることを確認してから、@Query("select distinct s from Shop s join fetch s.items")で1本に減らしてください。数え方を知ることは、直し方を知ることより重要です。

Web層だけを起動するテスト

@WebMvcTestでコントローラーだけを検証するテストを1つ書いてください。サービスは@MockitoBeanで代わりに差し込みます。

@WebMvcTestはサービスBeanを起動しないため、モックを差し込まないとコンテキストが起動しません。MockMvcで呼び出します。コンテキスト全体を毎回起動するとテストが遅くなり、遅くなると誰も実行しなくなります。

見えなかった3つのこと

08-notes.mdに3行以上書いてください。なぜコンストラクター注入なのか、N+1がなぜ起こり何で直したのか、@Transactionalが何を元に戻すのか(そして戻さない場合)を書きます。

本文に생성자、N+1、롤백が含まれている必要があります(韓国語の2つの語は、それぞれ「コンストラクター」と「ロールバック」を意味します)。ステップ6の2つの数字も併せて書いておくと、後で同じコードを見たときに判断が速くなります。