JPAのｎ+１問題および対策について

## はじめに
今回はJPAを使うことにより発生するn+1問題の意味と、解決方法についてご紹介します。
この記事で書いたコードは[こちら](https://github.com/crane93/qiita-article/tree/main/n-plus-one-query-problem/demo)でご確認してください！
## n+1問題とは？
### 意味
エンティティの間に`1:N`または`N:N`などの関係が存在する場合、
親のエンティティを参照すると(1個のクエリ)、取得した親のデータ数だけ子のエンティティを参照するSQLが発生する(n個のクエリ)ことです。
では、具体的な例で確認してみましょう。
### コードで確認
#### ER図
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2727545/be0b33c5-a504-5312-26d7-6ad82d070d95.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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2727545/9eaee282-8e0a-f105-a8a5-b39829f1f55e.png)
・`Youtuder`テーブルを参照するSQL文1個
・`Video`テーブルで3個の`Youtuder`を一つ一つ参照するためのSQL文3個
1+n問題が生成されました👏

ですが、これって`FetchType.EAGER`だから起きた問題なんでしょうか。
##### FetchType.LAZY
まず、`FetchType.LAZY`に変え、`YoutuderRepository.findAll()`を実行します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2727545/52007fea-9d0f-f85b-df30-636444eabf8c.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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2727545/ec08f6ae-8b1c-4274-ce63-f2deefb9abb4.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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2727545/9c85884e-966d-8603-059e-72539e6b8c83.png)
クエリが1個だけ発生しました！
### EntityGraph
`@EntityGraph`の`attributePaths`に書いたエンティティをFetchType.Eagerで参照して結果を返します。
`@EntityGraph`はouter joinでエンティティを参照します。
```
@EntityGraph(attributePaths = "videos")
@Query("select a from Youtuder a")
List<Youtuder> findAllEntityGraph();
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2727545/d6db9e2f-1090-2716-57e5-a7197cdf2b00.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を正しく使うために本格的に勉強する必要があると思いました。