@OneToMany
でリレーションを設定したJPA Entityに対し、Spring Data JPAのSpecificationで動的クエリを発行している。
N+1問題の回避のため、 JOIN FETCH
して findAll(Specification)
で検索していたが、ページングすることとなり、 findAll(Specification, Pageable)
に切り替えたところ、実行時に例外が発生するようになった。
原因と対応方法を調べたのでメモ。
問題
@OneToMany
でリレーションを設定したJPA Entityに対し、Spring Data JPAのSpecificationで動的クエリを発行している。
JpaRepository#findAll した場合、デフォルトでは @OneToMany
を付与したフィールドを参照したタイミングでリレーション先のエンティティ取得のSQLが実行され、N+1問題が発生する。
いくつか対応方法はあるが、 JOIN FETCH
することで1クエリで関連エンティティも取得できるため、 Specification#toPredicate
内で Root#fetch
し、 JpaSpecificationExecutor#findAll(Specification) で検索していた。
// Specificationをラムダ式でインスタンス化 return (root, query, cb) -> { root.fetch(EntityClass_.relatedEntities, JoinType.LEFT); // 検索条件を設定 }
同様の検索条件でページングすることとなり、単純に List
を返す findAll
から Page を返す JpaSpecificationExecutor#findAll(Specification, Pageable) に切り替えたところ、実行時に以下の例外が発生するようになった。
java.lang.IllegalArgumentException: org.hibernate.QueryException: query specified join fetching, but the owner of the fetched association was not present in the select list [...] [select count(generatedAlias0) from EntityClass as generatedAlias0 inner join fetch generatedAlias0.relatedEntities as generatedAlias1 ...]
対応
エラーメッセージは「フェッチしているのに、SELECTで使ってない」的な内容。JPQLが出力されているが、そこにも select count(関連元エンティティ)
が出力されている。
Page#getTotalElements
で使用する件数取得のために、SELECT句を COUNT
に変換したクエリが実行されるが、それにより例外が発生している模様。
エラーメッセージで検索すると spring data jpa specification join fetch is not working - Stack Overflow が出てきた。
その中のリンクから飛んだ、 java - Eager fetching in a Spring Specification - Stack Overflow に対応方法があった。
Specification#toPredicate
の第2引数の CriteriaQuery#getResultType
で戻り値の型を取得できる。
これが Long
であればカウントクエリとみなし、その場合はフェッチしないようにすればいい。
return (root, query, cb) -> { if (Long.class != query.getResultType()) { root.fetch(EntityClass_.relatedEntities, JoinType.LEFT); } // 検索条件を設定 }
この変更で、例外なく実行できるようになった。
振り返り
spring data specification join fetch
でWeb検索したら、そのものズバリなStack Overflowの質問も出てきた。
ここでは Long.class
以外に long.class
とも比較しているが、 Long.class
だけで動いている。
うまいこと解消しなければ、検索とカウントでクエリを分けようかと思っていたので、シンプルに対応できてよかった。
参考
JPAでのN+1問題と対応方法