Spring Data JPA / Hibernate の "failed to lazily initialize a collection" 解決方法|@Transactional と FetchType
Spring / Hibernate のエンティティに @OneToMany や @ManyToMany を付けておくと、関連レコードをゲッター経由でそのまま読めて魔法のように見えます。ところが、あるときそのゲッターが次の例外を投げます。
org.hibernate.LazyInitializationException: failed to lazily initialize a collection
本記事では、なぜこの例外が発生するのか、LAZY と EAGER の fetch type は実際にどう動くのか、そして推奨される @Transactional による修正方法と、代替案である FetchType.EAGER を解説します。
発生する仕組み
JPA ではエンティティのフィールドに @OneToOne / @OneToMany / @ManyToOne / @ManyToMany を付けることで、そのフィールドが外部キーに対応していることを永続化層に伝えられます。ここまでは楽です。問題は、このリレーションを「タイミングの悪いところで」辿ろうとしたときに起きます。
各マッピングアノテーションにはデフォルトの fetch type が定義されています。
FetchType.EAGER— 親エンティティと同じクエリで関連レコードも読み込むFetchType.LAZY— ゲッターを呼んだときに初めて読み込む(プロキシ経由で 2 回目のクエリを発行)
遅延読み込みが成立するのは、Hibernate のセッション(と、それが保持している DB 接続)が開いている間だけです。エンティティの参照を保持したままサービス / リポジトリメソッドの外に出て、そこでゲッターを呼ぶと、セッションはすでに閉じられているため 2 回目のクエリを発行できず、LazyInitializationException が投げられます。
よくあるのは、DAO / リポジトリからエンティティを受け取り、コントローラやテンプレート、あるいは元の Spring Bean の外にあるヘルパーから遅延フィールドにアクセスするパターンです。
アノテーション別のデフォルト fetch type:
| アノテーション | デフォルト fetch type |
|---|---|
@OneToOne | FetchType.EAGER |
@ManyToOne | FetchType.EAGER |
@OneToMany | FetchType.LAZY |
@ManyToMany | FetchType.LAZY |
つまりこの例外は、ほぼ常にデフォルトが LAZY の @OneToMany / @ManyToMany から発生します。
解決策:@Transactional
遅延コレクションにアクセスするメソッドに @Transactional を付けます。Spring がそのメソッドの実行中ずっと Hibernate セッションを開いたままにしてくれるので、ゲッターから発行される 2 回目のクエリも無事に走ります。読み取り専用の処理なら readOnly = true を付けておくと少しだけパフォーマンスが上がります。
package com.example.blog;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class BlogPostService {
@Autowired
private BlogPostRepository postRepository;
@Transactional(readOnly = true)
public void doSomething() {
postRepository.findById(...);
// ここで遅延コレクションにアクセス可能 — セッションは開いたまま
}
}
注意点:@Transactional は Spring のプロキシ機構で実装されているため、クラスの外から呼び出されたときだけ 有効になります。上の例で BlogPostService 内の別メソッドから doSomething() を呼んでも、プロキシを経由しないのでトランザクションは開始されません。
別解:FetchType.EAGER
エンティティ側で強制的に即時読み込みにする方法もあります。
package com.example.blog;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.OneToMany;
import java.util.List;
@Entity
public class BlogPost {
@OneToMany(fetch = FetchType.EAGER)
private List<BlogTag> tags;
}
関連レコードを最初のクエリで一気に読んでしまうので、あとでセッションが閉じても問題になりません。
代わりにパフォーマンスのコストを払います。BlogPost を読むたびに Hibernate は対応する BlogTag をすべて引いてくるため、呼び出し側が実際には使わない場合でも毎回取得されます。データ量が少なく毎回使うなら OK ですが、コメントや閲覧ログのように無制限に増えうるコレクションでは LAZY のままにして、アクセスパターン側を @Transactional で直すのが正解です。
javax.* と jakarta.*:名前空間の変更
本記事のコードは原文に合わせて javax.persistence と javax.transaction を使っています。Spring Boot 2.x なら問題ありませんが、Spring Boot 3.x や Jakarta EE 9 以降のランタイムを使っている場合は、すべての import を javax から jakarta に置き換える必要があります。
経緯は短くまとめるとこうです。
- Java EE → Jakarta EE。2017 年に Oracle が Java EE を Eclipse Foundation に移管。ライセンスの都合で
javax.*という名前空間を引き継げませんでした。 - Jakarta EE 9(2020 年 9 月リリース)でパッケージ名が機械的にリネーム。すべての
javax.*がjakarta.*に置換されました。API 自体に変更はありません。 - Spring Boot 2.x は
javax.*のまま(Jakarta EE 8 / Java EE 8 互換、Java 8 ベースライン)。 - Spring Boot 3.x(2022 年 11 月リリース)で
jakarta.*に移行し、Java 17 と Jakarta EE 9+ がベースラインになりました。
実務上の境界線は基本的に Spring Boot 2 と 3 の差 と考えて問題ありません。
| プロジェクトのベースライン | 永続化 / トランザクションの import |
|---|---|
| Spring Boot 2.x、旧 Spring / Jakarta EE 8 | javax.persistence.*, javax.transaction.* |
| Spring Boot 3.x、Jakarta EE 9+ | jakarta.persistence.*, jakarta.transaction.* |
どちらの名前空間でもアノテーション名、パラメータ、挙動はすべて同一です。マイグレーションの多くは javax → jakarta の単純な置換で済みます。
補足:Spring の @Transactional と JPA の違い
最近の Spring は以下のどちらのアノテーションでもトランザクションを開始してくれます(Spring Boot 3.x なら jakarta.transaction に置き換え)。
javax.transaction.Transactionalorg.springframework.transaction.annotation.Transactional
ただし Spring のほうには JPA 版にない属性がいくつかあります。上記の修正例で使った readOnly = true は Spring 版にしか存在しません。rollbackFor / noRollbackFor も、Spring 版はクラス名を文字列で渡せますが、JPA 版は Class<?> リテラルしか受け付けません。
Spring の @Transactional だけにある属性:
boolean readOnlyint timeoutString timeoutStringString[] rollbackForClassNameString[] noRollbackForClassName
迷ったら、Spring ベースのコードベースでは org.springframework.transaction.annotation.Transactional を使っておくのが無難です。