org.hibernate.LazyInitializationException の修正: コレクションの遅延初期化に失敗 - セッションなし

intermediate Java2026-03-24| Java 8以上、Hibernate 5.x/6.x、Spring Boot 2.x/3.x、JPA、各種リレーショナルデータベース(MySQL、PostgreSQL、H2)

Error Message

org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: could not initialize proxy - no Session
#java#hibernate#jpa#lazy-loading#spring#session

エラーの内容

org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: could not initialize proxy - no Session

トランザクション内でエンティティを取得し、セッションが閉じられた後に、遅延ロードされたリレーションシップへのアクセスが試みられた場合に発生します。Hibernateはデータベースに戻ることができません — セッションはすでに終了しています。

典型的なシナリオ:サービスメソッドがエンティティを返し、トランザクションが終了した後に、コントローラーやJSONシリアライザーがentity.getOrders()にアクセスします。その瞬間に例外が発生します。

発生原因

JPA/Hibernateは、@OneToManyおよび@ManyToManyのコレクションをデフォルトで遅延ロードします。実際にアクセスするまでフェッチをスキップすることで、メモリを節約し、不要なクエリを避けます。ただし、Session(またはEntityManager)が閉じられた後に遅延ロードをトリガーすると、この例外がスローされます。

主な原因:

  • @Transactionalメソッドの外で遅延コレクションにアクセスしている
  • open-session-in-viewが無効になっている — これは本番環境での正しい設定です
  • サービスのトランザクション終了後、コントローラー層でエンティティをJSONにシリアライズしている
  • アクティブなセッションを持たないバックグラウンドスレッド内でゲッターを呼び出している

解決手順

方法1:フェッチジョインをクエリに使用する(推奨)

2回ではなく1回のクエリで済みます — フェッチジョインにより、Hibernateは親エンティティと一緒にコレクションをロードします。余分なラウンドトリップもなく、後から予期しない例外も発生しません。

// リポジトリ内
@Query("SELECT o FROM Order o JOIN FETCH o.items WHERE o.id = :id")
Optional<Order> findByIdWithItems(@Param("id") Long id);

JPA Criteria APIを使う場合:

CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Order> query = cb.createQuery(Order.class);
Root<Order> root = query.from(Order.class);
root.fetch("items", JoinType.LEFT);
query.select(root).where(cb.equal(root.get("id"), orderId));

セッション内で、必要なデータを一度に正確にロードします。すべてがロード済みなので、トランザクションが閉じられた後に問題が発生することはありません。

方法2:@Transactionalでトランザクションのスコープを拡張する

自分のサービスコード内で遅延アクセスが発生する場合は、必要なものを初期化するのに十分な時間、セッションを開いたままにします。

@Service
public class OrderService {

    @Transactional  // メソッド全体でセッションを開いたままにする
    public OrderDTO getOrderWithItems(Long orderId) {
        Order order = orderRepository.findById(orderId)
            .orElseThrow(() -> new EntityNotFoundException());
        
        // 安全:ここではセッションがまだアクティブ
        order.getItems().size(); // トランザクション内で遅延ロードをトリガー
        
        return toDTO(order); // セッションが閉じる前にDTOに変換
    }
}

DTOへの変換はトランザクションで行ってください — 終了後ではありません。生のエンティティを返してコントローラーが後から遅延フィールドにアクセスするのが、まさにこのエラーの原因です。

方法3:DTOを使用してエンティティの公開をやめる

サービスメソッドからエンティティを直接返すことが、この問題の出発点になることがほとんどです。トランザクション内でDTOにマッピングすれば、遅延ロードについて考える必要はなくなります — ロードするものが何も残っていないため、セッションは安全に閉じられます。

@Transactional(readOnly = true)
public OrderDTO findOrder(Long id) {
    Order order = orderRepository.findByIdWithItems(id) // フェッチジョインクエリ
        .orElseThrow(EntityNotFoundException::new);
    
    return new OrderDTO(
        order.getId(),
        order.getStatus(),
        order.getItems().stream()
            .map(item -> new ItemDTO(item.getId(), item.getName(), item.getPrice()))
            .toList()
    );
}

方法4:FetchTypeをEAGERに変更する(慎重に使用)

Hibernateが常にコレクションをロードするように設定できます:

@OneToMany(mappedBy = "order", fetch = FetchType.EAGER)
private List<Item> items;

シンプルですが、実際のコストがあります。Orderをロードするたびに、Hibernateはすべてのitemsもロードします — 必要がない場合でも。3つのEAGERリレーションシップを持つエンティティは、1行を取得するだけで4つのクエリを実行する可能性があります。50件のOrderのリストでは、200件以上のクエリになります。常に必要な小さなコレクションにのみ使用してください。

方法5:単発のケースにHibernate.initialize()を使用する

クエリを再構築できない場合があります。そのような場合は、トランザクション内にいる間に明示的に初期化を強制します:

@Transactional
public Order loadOrderWithItems(Long id) {
    Order order = orderRepository.findById(id).orElseThrow();
    Hibernate.initialize(order.getItems()); // 明示的な初期化
    return order;
}

これは局所的な修正です。機能しますが、まずフェッチジョインを検討してください — よりクリーンで、クエリ内で意図が明確になります。

方法6:open-in-viewを有効にする(開発環境のみ)

Spring Bootはデフォルトでspring.jpa.open-in-view=trueを設定します。これにより、HTTPリクエスト全体でセッションが開いたままになり、リクエストサイクルのどこでも遅延ロードが機能します。開発中は問題を隠しますが、必要以上に長くデータベース接続を保持し続けます — 高負荷時には深刻な問題になります。

# application.properties
spring.jpa.open-in-view=false  # 本番環境では無効にする

無効にしてください。すぐに例外が表示され、問題を隠すのではなく、根本的な原因を修正せざるを得なくなります。

検証

リリース前に簡単な動作確認を行います:

  • spring.jpa.open-in-view=falseであることを確認する — そうでなければ非現実的な条件でテストしていることになります
  • 例外が発生していたエンドポイントにアクセスし、レスポンスを確認する
  • アプリケーションログに残っているLazyInitializationExceptionのスタックトレースがないかスキャンする
  • 修正によって発生したN+1問題を検出するためにSQLログを有効にする:
# application.properties
spring.jpa.show-sql=true
logging.level.org.hibernate.SQL=DEBUG
logging.level.org.hibernate.orm.jdbc.bind=TRACE

ログに1エンティティあたり複数のクエリが表示される場合はN+1問題です。フェッチジョインに切り替えてください。

クイックリファレンス

  • ループ内でコレクションにアクセス → N+1リスク:フェッチジョインまたは@EntityGraphを使用する
  • コントローラーでのシリアライズが失敗する@Transactionalサービスメソッド内でDTOに変換する
  • バックグラウンドスレッドがクラッシュするTransactionTemplateで新しいトランザクションを明示的に開く
  • テスト環境のみ:テストクラスに@Transactionalを追加して、アサーション中にセッションを開いたままにする
// フェッチジョインの代替としてEntityGraph — リポジトリをクリーンに保つ
@EntityGraph(attributePaths = {"items", "items.product"})
Optional<Order> findById(Long id);

ルールはシンプルです:初期化されていないプロキシが付いたままのHibernateエンティティをトランザクション境界をまたいで渡さないこと。必要なデータを決め、セッション内ですべてロードし、アプリケーションの残りの部分にはプレーンなDTOを渡してください。

Related Error Notes