Springの@AsyncとLazy Fetch

Spring/Spring Bootと、JPA(Hibernate)を使っているプロジェクトにおいて、Lazy Fetchを指定しているParent → Childrenエンティティへのアクセスにおいて、以下の

org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: xxx.yyy.Parent.children, could not initialize proxy - no Session

エラーが起きた時の原因と対処を簡単にメモしておきます。

状況と原因

LazyFetchが指定されているので、関連エンティティ(chidren)の取得を行ったタイミングでDBへのアクセスが発生します。で、その際にトランザクションが無効になっていて、エラーとなっています。
まぁ、エラーメッセージのまんまですね…。

状況的には Controller - Service1 - Service2 - Repository の構成をとっていて、 Service1 のメソッドに @Async をつけており、非同期で実行することを想定してます。

要所だけを抜粋したコードにて、比較用に同期バージョンと非同期バージョンの両方を並べて記載します。
この例だと非同期バージョンのみ、当該エラーが起きました。

Controller

private final Service1 service1

@GetMapping("/sync")
public String fooSync() {
    return service1.exec();
}

@GetMapping("/async")
public String fooAsync()  throws Exception {
    CompletableFuture<String> future = service1.execAsync();
    CompletableFuture.allOf(future).join(); // 結果を待つ(この例だと、非同期の意味がなくなるが…)
    return future.get();
}

Service1

private final Service2 service2;

public String exec() {
    Parent parent = service2.query();

    // parent.getChildren()

    return parent.toString();
}

@Async
public CompletableFuture<String> execAsync() {
    Parent parent = service2.query();

    // parent.getChildren() 

    return CompletableFuture.completedFuture(parent.toString());
}

Service2

private ParentRepository repository;

public Parent query() {
    return repository.findById("123");
}

対処(ダメな例)

  • Service2側のクラス/メソッドに @Transactional をつける
  • Service2側のクラス/メソッドに @Transactional(propagation = Propagation.REQUIRES_NEW) をつける

改めて書いてみると当たり前な感じですが、LazyFetchが呼ばれるのはService1側なので、Service2でトランザクションをつけても意味がありません。

対処

以下のどちらかの対処をすればOKです。後者についてはロジックの修正になりますね。

  • Service1側のクラス/メソッドに @Transactional をつける
  • Service2側のメソッド内部でLazy Fetchが行われるよう、関連エンティティの取得もやっておく。

ちなみに、実務で遭遇したのはしょうもないオチだったのですが、 @Async をつけたメソッド内でparallelストリームを使っていたりなんかすると、これまたやはりストリーム内の各処理(スレッド)でそれぞれトランザクションが有効になってないとならないです。

(非同期、並列処理、JPA、このあたりを絡んでくるとハマりどころが多いので、設計・技術選定は定期的に見直さないとなぁ。。)

参考サイト

Spring LazyInitializationException: no Session - Qiita

Loading Lazy loaded Entity in Async Thread in Spring Boot | by Prasim Jain | Medium