はじめに
今回はJPAを使うことにより発生するn+1問題の意味と、解決方法についてご紹介します。
この記事で書いたコードはこちらでご確認してください!
n+1問題とは?
意味
エンティティの間に1:NまたはN:Nなどの関係が存在する場合、
親のエンティティを参照すると(1個のクエリ)、取得した親のデータ数だけ子のエンティティを参照するSQLが発生する(n個のクエリ)ことです。
では、具体的な例で確認してみましょう。
コードで確認
ER図

1人のユーチューバーが複数の動画を持っているという意味のシンプルなテーブル構成です。
データ追加
insert into youtuder(youtuder_id, youtuder_name) values(1, "you0");
insert into youtuder(youtuder_id, youtuder_name) values(2, "you1");
insert into youtuder(youtuder_id, youtuder_name) values(3, "you2");
insert into video(title, youtuder_id) values("n+1 0", 1);
insert into video(title, youtuder_id) values("n+1 1", 2);
insert into video(title, youtuder_id) values("n+1 2", 3);
エンティティ作成
public class Youtuder{
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer youtuderId;
@Column
private String youtuderName;
@OneToMany(mappedBy="youtuder", cascade=CascadeType.ALL, fetch = FetchType.LAZY)
private List<Video> videos;
}
public class Video{
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer videoId;
@Column
private String title;
@Column(name = "youtuder_id")
private Integer youtuderId;
@ManyToOne(fetch = FetchType.EAGER) //★
@JoinColumn(name="youtuder_id", insertable=false, updatable=false)
private Youtuder youtuder;
}
エンティティ取得
FetchType.EAGER
YoutuderRepository.findAll()でYoutuderエンティティを参照した結果、以下のSQLが生成されました。

・Youtuderテーブルを参照するSQL文1個
・Videoテーブルで3個のYoutuderを一つ一つ参照するためのSQL文3個
1+n問題が生成されました👏
ですが、これってFetchType.EAGERだから起きた問題なんでしょうか。
FetchType.LAZY
まず、FetchType.LAZYに変え、YoutuderRepository.findAll()を実行します。

クエリが1個だけ生成されました。ならFetchType.LAZYではn+1問題が発生しないですね!
次はYoutuderRepository.findAll()した結果からVideoのtitleを取得するメソッドを実行してみます。
private List<String> extractTitle(List<Youtuder> youtuders){
return youtuders.stream()
.map(y -> y.getVideos().get(0).getTitle())
.collect(Collectors.toList());
}
関連するエンティティを参照する処理をしたらFetchType.LAZYでもn+1問題が発生してしまいましたね。
原因
jpaRepositoryに定義したインターフェースメソッドを実行するとJPAはメソッド名を分析しJPQLを生成します。
JPQLはSQLをエンティティオブジェクトとフィールド名でクエリを生成します。
そのため、JPQLはfindAll()というメソッドを実行すると、エンティティ間の関係を無視して該当のエンティティを参照するselect * from Youtuderだけ生成します。もし、関連のエンティティが必要ならFetchTypeで指定した時点で別途に関連のエンティティを参照するクエリを生成します。
n+1問題はなぜよくないか
テストしたデータは数が少ないためn+1問題が発生しても総4件のクエリだけ生成されました。
しかし、業務レベルで考えると何百、何千のデータが存在するのは当たり前です。
そこでn+1問題が発生したら、サーバーに莫大な負荷がかかりアプリケーションの性能を落ちることになります。
対策
join fetch
JPQLを使用しjoin fetchでエンティティを参照します。
youtuder参照する際に、inner joinでvideoも一緒に参照する方法です。
@Query("select o from Youtuder o join fetch o.videos")
List<Youtuder> findAllJoinFetch();
EntityGraph
@EntityGraphのattributePathsに書いたエンティティをFetchType.Eagerで参照して結果を返します。
@EntityGraphはouter joinでエンティティを参照します。
@EntityGraph(attributePaths = "videos")
@Query("select a from Youtuder a")
List<Youtuder> findAllEntityGraph();
注意点
上記の方法はデカルト積が発生し、Videoの数だけYoutuderが重複します。
解決方法としては
・JPQLにDISTINCTを指定
→join fetch, @EntityGraph両方で利用可能
→join fetch:@Query("select DISTINCT o from Youtuder o join fetch o.videos")
→@EntityGraph:@Query("select DISTINCT a from Youtuder a")
・@OneToManyのフィールドのタイプをSetに指定
→private Set<Video> videos = new LinkedHashSet<>() //LinkedHashSetを使用してエンティティの並び順を厳守
最後に
単純にSQLを書かなくて便利だからむやみにJPAを使ってましたが、やみ深いところがありましてびっくりしました。
これからはJPAを正しく使うために本格的に勉強する必要があると思いました。


