1. test 1
  2. test 2
  3. test 3
  4. test 4
  5. test 5
  6. test 6
コース一覧
Spring Boot入門
N+1問題

Spring Boot入門

Java で Web API サーバーを作るための Spring Boot の入門コースです。DI コンテナ、REST コントローラ、リクエスト検証、レイヤ分割、Spring Data JPA、例外ハンドリング、テストまでを扱います。毎レッスン、Spring が裏側でやっている判断そのものを pure Java の関数として自分で書き、フレームワークが何を肩代わりしてくれているのかを手触りで理解できる構成にしました。約 12 時間 (1 日 30 分 × 24 日) で 48 レッスンを修了できます。

1
Spring Bootとは何か
01. Spring Bootで何が作れるのか10分
02. サーブレットからフレームワークへ15分
03. ルーティング表という発想15分
04. プロジェクトの構成10分
05. 起動のしくみと自動設定15分
06. パスのマッチング15分
2
DIコンテナ
01. newで書くと何が困るのか15分
02. コンストラクタインジェクション15分
03. インターフェースへの依存15分
04. @Componentと@Bean15分
05. 自作のDIコンテナ20分
06. Beanのスコープとライフサイクル15分
3
RESTコントローラ
01. RESTという設計方針15分
02. @RestControllerの役割15分
03. @GetMappingとパス変数15分
04. クエリパラメータの受け取り15分
05. @PostMappingとリクエストボディ15分
06. ステータスコードの選び方15分
07. ResponseEntityで組み立てる15分
4
リクエストとレスポンスの変換
01. JSONとオブジェクトの往復15分
02. DTOとエンティティを分ける15分
03. recordでDTOを書く15分
04. 入力検証の基本15分
05. 検証エラーをまとめて返す15分
06. アノテーションによる検証15分
5
レイヤ構成とビジネスロジック
01. 3層アーキテクチャ10分
02. @Serviceに置くもの15分
03. 状態遷移をルールにする15分
04. 料金計算のような業務ロジック15分
05. @Transactionalとは何か15分
06. トランザクションの巻き戻し15分
6
データアクセス
01. @Entityとテーブルの対応15分
02. リポジトリという抽象15分
03. メソッド名からクエリを作る20分
04. 検索条件の組み立て15分
05. ページングと並び替え15分
06. N+1問題15分
7
例外処理と横断的関心事
01. 例外をHTTPに変換する15分
02. @ExceptionHandlerと@ControllerAdvice15分
03. エラーレスポンスの形を決める15分
04. フィルタとインターセプタ15分
05. ログとリクエストの追跡15分
06. 設定値の外部化15分
8
テストと仕上げ
01. テストの種類と使い分け15分
02. Serviceの単体テスト15分
03. モックで依存を差し替える15分
04. MockMvcでエンドポイントを試す15分
05. 本番へ出すための次の一歩10分

N+1問題

このレッスンでは、JPA で最も有名な性能問題である N+1 問題を、発行されるクエリの本数として数えられるようになります。

一覧を1回取っただけのつもりが

ユーザーとその投稿を一覧で返す、よくある処理を書きます。

Java

List<User> users = userRepository.findAll(); for (User user : users) { System.out.println(user.getName() + " " + user.getPosts().size()); }

このコードでリポジトリを呼んでいるのは1回だけです。しかし SQL のログを見ると、次のようになっています。

SQL クエリ

SELECT * FROM users; -- 1本目 SELECT * FROM posts WHERE user_id = 1; -- 2本目 SELECT * FROM posts WHERE user_id = 2; -- 3本目 SELECT * FROM posts WHERE user_id = 3; -- 4本目

ユーザーが100人いれば、クエリは101本です。親を取る1本と、子を取る N 本。これが N+1 問題です。100本のクエリが1本あたり2ミリ秒でも、それだけで0.2秒が消えます。1000人になれば2秒です。

なぜ増えるのでしょうか

原因は遅延ロードです。@OneToMany の既定のフェッチ方式は LAZY で、関連するコレクションは実際に触られるまで読み込まれません。findAll() の時点では posts の中身は空の入れ物のままで、user.getPosts() が呼ばれた瞬間に初めて SELECT が飛びます。ループの中でそれをやるので、回数分だけクエリが増えます。

ここで「では FetchType.EAGER にすればよいのでは」と考えたくなりますが、これは解決になりません。EAGER にしても、多くの場合は親を取った後で子を1件ずつ読みに行くだけで、本数は変わりません。しかも常に読み込まれるようになるため、投稿を必要としない画面でも余計なクエリが飛びます。既定の LAZY のままにしておくのが正解です。

1本にまとめる方法

直し方は「親と子を1回の SQL で取ってくる」ことです。JPQL で JOIN FETCH を書きます。

Java

@Query("SELECT DISTINCT u FROM User u JOIN FETCH u.posts") List<User> findAllWithPosts();

あるいは @EntityGraph を使うと、JPQL を書かずに同じことができます。

Java

@EntityGraph(attributePaths = "posts") List<User> findAll();

どちらも結合した1本のクエリになるので、親が何件あってもクエリは1本です。方式ごとの本数をまとめます。

方式親 N 件のときのクエリ本数
遅延ロードのままループで触るN + 1
JOIN FETCH1
@EntityGraph1
バッチサイズ指定1 + 切り上げ(N ÷ バッチサイズ)

JOIN FETCH に DISTINCT を書いているのは、結合の結果として親の行が子の件数だけ重複するためです。これを取り除かないと、ユーザーが投稿の数だけリストに現れます。

気づき方

N+1 は動作としては正しいので、テストは通ってしまいます。件数が少ない開発環境では体感もできません。本番でデータが増えて初めて表面化する、という現れ方をします。開発中に見つけるには、SQL をログに出す設定を入れておき、一覧の処理でクエリが件数に比例して増えていないかを目で確かめるのが確実です。

演習で書くもの

演習では、親の件数とフェッチ方式から、発行されるクエリの本数を計算します。遅延ロードなら N+1、JOIN FETCH なら常に1本です。親が0件のときにどうなるかも考えてください。子を取るクエリは親1件につき1本なので、親が0件なら子のクエリは飛びません。

要件

  1. fetchMode が lazy のときは parentCount + 1 を返すこと
  2. fetchMode が join のときは parentCount にかかわらず 1 を返すこと
  3. lazy でも join でもない fetchMode のときは -1 を返すこと

入出力例

countQueries(100, "lazy") → 101 countQueries(100, "join") → 1 countQueries(1, "lazy") → 2 countQueries(0, "lazy") → 1 countQueries(0, "join") → 1 countQueries(10, "eager") → -1

ヒント

文字列の比較は equals を使います。== では正しく判定できません

親が0件のときは lazy でも parentCount + 1 が 1 になるので、特別扱いは要りません

戻り値は int なので、そのまま数値を返してください

生田 陸人
監修生田 陸人
ゆめさくエンジニア / 現役ソフトウェアエンジニア監修者プロフィールを見る →
編集 ゆめさく編集部·公開 2026/08/09

関連レッスン

  • 例外をHTTPに変換する

    例外の種類からステータスコードを決める処理を実装します。

  • テストの種類と使い分け

    @SpringBootTest と @WebMvcTest の違いを確認します。

  • ページングと並び替え

    ページ番号と件数から取り出す範囲を計算します。

  • 検索条件の組み立て

    指定された条件だけを WHERE 句に足す処理を実装します。

このレッスンに出てくる用語

意味があいまいなまま進んだ語は、ここから読み直せます。

  • 処理計算や代入を表す長方形
  • リポジトリプロジェクトの保管場所
  • SQLデータベースを操作するための共通言語
  • 関連クラス間のつながりを表す直線
  • ループ繰り返し処理。矢印で戻すか専用記号で示す
  • JOIN複数テーブルをくっつけて 1 つの結果に並べる SQL
  • fetchブラウザ標準の HTTP クライアント API
  • リスト順序付きで複数の値を扱うデータ構造
main.java
学習モード
エディタを読み込んでいます

メモ

N+1問題

⌘S で保存