0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

JPAのn+1問題および対策について

Last updated at Posted at 2022-09-19

はじめに

今回はJPAを使うことにより発生するn+1問題の意味と、解決方法についてご紹介します。
この記事で書いたコードはこちらでご確認してください!

n+1問題とは?

意味

エンティティの間に1:NまたはN:Nなどの関係が存在する場合、
親のエンティティを参照すると(1個のクエリ)、取得した親のデータ数だけ子のエンティティを参照するSQLが発生する(n個のクエリ)ことです。
では、具体的な例で確認してみましょう。

コードで確認

ER図

image.png
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が生成されました。
image.png
Youtuderテーブルを参照するSQL文1個
Videoテーブルで3個のYoutuderを一つ一つ参照するためのSQL文3個
1+n問題が生成されました👏

ですが、これってFetchType.EAGERだから起きた問題なんでしょうか。

FetchType.LAZY

まず、FetchType.LAZYに変え、YoutuderRepository.findAll()を実行します。
image.png
クエリが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());
}

image.png

関連するエンティティを参照する処理をしたら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();

image.png
クエリが1個だけ発生しました!

EntityGraph

@EntityGraphattributePathsに書いたエンティティをFetchType.Eagerで参照して結果を返します。
@EntityGraphはouter joinでエンティティを参照します。

@EntityGraph(attributePaths = "videos")
@Query("select a from Youtuder a")
List<Youtuder> findAllEntityGraph();

image.png
こっちもクエリが1個だけ発生しました!

注意点

上記の方法はデカルト積が発生し、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を正しく使うために本格的に勉強する必要があると思いました。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?