🧠【初心者向け】JPA の遅延読み込み(Lazy Load)をやさしく解説(図解あり)
JPA / Hibernate を使い始めた初心者が必ずぶつかるテーマ、それが 遅延読み込み(Lazy Load)。
- 「Lazy = 遅延ってどういう意味?」
- 「N+1問題ってなに?」
- 「なぜエンティティの中に“プロキシ”が入るの?」
- 「セッションが閉じると LazyInitializationException が出る理由は?」
- 「EAGER と何が違う?」
この記事では
図解 × 初心者向けのやさしい説明 × 実務で役立つ深掘り
の3つを組み合わせて、Lazy Load の正体を理解します。
結論
✔ Lazy Load は「必要になった瞬間にだけ関連データを取りに行く」仕組み
✔ 実体は Hibernate が生成する “プロキシオブジェクト(代理)”
✔ プロキシの中には “DBに取りに行くためのロジック” が隠れている
✔ セッションが閉じると Lazy initialization 不能
✔ N+1問題は Lazy Load とコレクションが主因
Lazy Load と EAGER Load の違い(まずはここから)
まずは2つのロード戦略を整理しましょう。
✔ Lazy(遅延読み込み)
必要になるまで関連データを取らない。
User を取得
→ getOrders() した瞬間に Orders を取りに行く
✔ EAGER(即時読み込み)
最初に関連データも全部取る。
User を取得
→ Orders も JOIN して最初のクエリで全取得
🔹 図解(短線版)
Lazy:
User ── getOrders() → SQL実行
EAGER:
User(取得時) → Ordersも同時にSQL
Lazy Load の正体:プロキシ(Proxy)とは?
Lazy Load の仕組みは Hibernate が生成するプロキシ によって実現されています。
つまり、関連エンティティを普通に入れているように見えて
実際には「代理オブジェクト」が入っている。
🔹 図解(短線版:プロキシの正体)
User
├ id
├ name
└ orders → Proxy(中身は空)
この Proxy の中に👇のような機能が入っている:
- DBに問い合わせるロジック
- セッション管理
- 初回アクセス判定
なぜ Lazy Load で SQL が複数回走るのか?(N+1問題)
例:ユーザー一覧(10人)を取得
SELECT * FROM users;
→ その後、for文で orders を参照すると…
for (user : users) {
user.getOrders(); ← ここで10回 SQL
}
合計 SQL:
- 1(ユーザー一覧)
- 10(ユーザーごとの orders)
→ 合計11回(N+1)
🔹 図解(短線版)
SELECT users; ← 1回
SELECT orders WHERE user_id=1;
SELECT orders WHERE user_id=2;
...
SELECT orders WHERE user_id=10;
LazyInitializationException が起きる理由
Spring Boot + JPA では
トランザクション外で Lazy を触ると この例外が出ます👇
could not initialize proxy - no Session
つまり:
- getOrders() した瞬間に SQL を打とうとした
- でもセッション(Persistence Context)が閉じている
- よってプロキシが初期化できない → エラー
🔹 図解
Controller(セッション外)
↓
user.getOrders() → プロキシがDBに行けない → 例外
どうすれば問題を回避できるか?
① トランザクション内でアクセスする
Service 層で @Transactional をつける。
② JOIN FETCH を使う
Lazy のまま “必要なときだけ” 強制 EAGER にする技。
@Query("SELECT u FROM User u JOIN FETCH u.orders")
→ 最初のSQLで Orders もまとめて取得。
③ DTO で必要なデータだけ抽出
Query で DTO を返す方式。
④ EntityGraph を使う
JPA標準の方法。
Hibernate の内部構造:プロキシ生成までの流れ
内部の動きはこう👇
1. エンティティをロード
2. 関連データ(OneToManyなど)は “まだ取らない”
3. フィールドに Proxy をセット
4. get○○() が呼ばれた瞬間に SQL を発行
5. 初期化(第一次取得)
6. 以後はキャッシュされたデータを返す
🔹 図解(短線版)
User.orders = Proxy
↓ getOrders()
DBアクセス → 結果セット → 初期化 → コレクションとして返る
Lazy Load は悪者なのか?
✔ Lazy はデフォルトで最適
✔ N+1問題は「使い方の問題」
✔ JOIN FETCH を使えば大体解決
✔ 大量データを抱えるエンティティでは Lazy が必須
実務でのベストプラクティス
✔ EAGER を安易に使わない
→ 予期せぬ JOIN 地獄になる。
✔ 必要なときは JOIN FETCH で都度取る
→ パフォーマンスも整う。
✔ DTO Query を合わせて使う
→ 特に画面表示用で有効。
✔ Service層で @Transactional をつける
→ Lazy の初期化を安定させる。
✔ Repository → Entity の責務、Service → ビジネスロジック
→ Controller で Lazy を触らない。
ここまでのまとめ(初心者向け)
- Lazy は「必要なときにだけ取得する」仕組み
- 正体は Hibernate のプロキシ
- N+1問題は Lazy + コレクションで起きやすい
- セッション外で Lazy を触ると例外
- JOIN FETCH で一発取得が可能
Deep Friendly Tech の一言(後書き)
Lazy Load は、JPA/Hibernate を理解するうえで
最大の壁であり、突破すると一気に理解が進む概念 です。
この記事を読んだことで、
- 「プロキシ」
- 「遅延読み込み」
- 「セッションの寿命」
- 「N+1問題」
- 「JOIN FETCH」
これらがすべて一本の線につながるようになっていれば
あなたはもう JPA の基礎を完全に理解しています。

コメント