【Java】Spring Data JPAでN+1問題が起きないコーディングをしよう。

# N+1問題とは
**1対多**の関係にある2テーブルの情報を紐づけて取得する際に、**必要以上にSQLが実行されてしまう**こと。
　　
　　対象テーブル(School)
　　関連テーブル(Student) 

具体的には、対象テーブル(School)に紐づく**関連テーブル(Student)を取得するSQL**が必要以上の回数で実行されてしまう。
 
# SQLでN+1問題を解説

1つのSchoolに対して、複数のStudentが紐づく。
|Schoolテーブル|
|----|
|id|
|name|

|Studentテーブル|
|----|
|id|
|name|
|school_id **<FK>**|

JOINを使えば、全Schoolとそれに紐づくStudentを１つのSQLで取得できる👍
```sql
SELECT * FROM School sc JOIN Student st ON sc.id = st.school_id;
```
これで話は終わり...ではなく、N+1問題の場合はどういうSQLになるかというと、次のようなSQLとなる。
```sql
-- 最初に、全Schoolを取得
SELECT * FROM School;
-- 次に、取得したSchoolの数だけ下記のSQLを実行。各Studentを取得する。
SELECT * FROM Student where school_id = ?
```
N+1の「1」= 全Schoolを「1」回で取得。
N+1の「N」= Schoolの数(N)分だけ、Studentを取得するSQLを「N」回実行する。
	
これがN+1問題。

# Spring Data JPA利用時にN+1問題を回避する
SQLを**自分で書く**分にはN+1問題は容易に回避できるが、SQLが**自動生成される**Spring Data JPAでは、気を付けて実装しないとN+1問題が起きてしまう。
	
N+1問題が起きないよう、Spring Data JPAでは下記の２つを意識して実装する必要がある。

1. fetchType
2. JOIN FETCH

## fetchType
Entityクラス内で、関連エンティティに付与する@**OneToOne**, @**OneToMany**, @**ManyToOne**, @**ManyToMany** のオプションの１つ。
```java:SchoolEntity.java
@OneToMany(fetch = FetchType.LAZY, mappedBy = "school")
private Set<StudentEntity> students = new HashSet<>();
```
|fetchType|意味|
|----|----|
|EAGER|対象テーブル取得時に関連テーブルも取得するSQLも実行する。|
|LAZY|関連テーブルのフィールドを参照した時に、関連テーブルを取得するSQLを実行する。|

### EAGER|対象テーブル取得時に関連テーブルも取得するSQLも実行する。とは？
下記の処理が実行されたタイミングで
```java
List<SchoolEntity> schoolEntities = 
	entityManager.createQuery("SELECT school FROM SchoolEntity school").getResultList();
```
下記のSQLが自動生成＆発行される。
（※実際に生成されるSQLはもうちょい長めなので説明用に簡単にしてます。）
```sql
-- 対象テーブル(school)取得用のSQL がまず実行される。
SELECT * FROM school;
-- 関連テーブル(student)取得用のSQL がN回(Schoolの数)実行される。
SELECT * FROM student WHERE school_id = 1;
SELECT * FROM student WHERE school_id = 2;
   --
SELECT * FROM student WHERE school_id = N;
```

### LAZY|関連テーブルのフィールドを参照した時に、関連テーブルを取得するSQLを実行する。とは？
下記の処理が実行されたタイミングで
```java
List<SchoolEntity> schoolEntities = 
	entityManager.createQuery("SELECT school FROM SchoolEntity school").getResultList();
```
対象テーブル(school)に対するSQLのみ実行される。
```sql
SELECT * FROM school;
```
その後、下記のように`List<SchoolEntity> schoolEntities`内の`StudentEntity`のフィールドにアクセスする処理が実行されると
```java
return schoolEntities.stream()
		.map(e -> modelMapper.map(e, SchoolResponse.class))
        .collect(Collectors.toList());
```
関連テーブル(student)を取得するSQLが流れる。
```sql
SELECT * FROM student WHERE school_id = 1;
SELECT * FROM student WHERE school_id = 2;
   --
SELECT * FROM student WHERE school_id = N;
```
	
fetchTypeのLAZYとEAGER、いずれも**後述するJOIN FETCHを使わないとN+1問題が発生してしまう。**
	
### どちらでもN+1問題が発生するならどっちでもいいのでは？
結論：**常にLAZYを設定しよう。EAGERは設定しない。**
理由：**EAGERだと、関連テーブル不要のときも関連テーブル取得用SQLが実行されるから。**

### fetchTypeのデフォルト値

|アノテーション|fetchTypeのデフォルト値|
|----|----|
|@OneToOne|EAGER|
|@OneToMany|LAZY|
|@ManyToOne|EAGER|
|@ManyToMany|LAZY|
	
## JOIN FETCH
この記事の主役。**N+1問題を解決するやつ。**

下記のようにJOIN FETCHを用いたJPQLを記述すると、このチャプターの最初にあげたようなSQL、すなわち**全Schoolとそれに紐づくStudentを取得するSQL**が自動生成＆実行される。
```sql:LEFT JOINを使ったJPQL
SELECT DISTINCT school FROM SchoolEntity school JOIN FETCH school.students
```
```sql:生成されるSQL(一部説明用に簡潔にしてます)
SELECT * FROM School sc JOIN Student st ON sc.id = st.school_id;
```

# 結論
1. fetchTypeは常に「LAZY」にする。
2. 対象テーブルと共に関連テーブルを取得したい場合のみ、LEFT JOINで取得する。

# 裏付けのための検証
「fetchType」と「JPQLでJOIN FETCH実行」の組み合わせで検証してみた。
||fetchType|JPQLでJOIN FETCH実行|SQL実行結果|関連テーブルを取得するSQL実行タイミング|
|----|----|----|----|----|
|1|EAGER|する|**1回のSQLで対象テーブル＋関連テーブルを取得する。**|対象Entity取得時|
|2|EAGER|しない|**関連テーブルを取得するSQLをN回実行する。**|対象Entity取得時|
|3|LAZY|する|**1回のSQLで対象テーブル＋関連テーブルを取得する。**|対象Entity取得時|
|4|LAZY|しない|**関連テーブルを取得するSQLをN回実行する。**|関連エンティティのフィールド参照時|

## N+1問題が発生するケース
||fetchType|JPQLでJOIN FETCH実行|SQL実行結果|関連テーブルを取得するSQL実行タイミング|
|----|----|----|----|----|
|2|EAGER|しない|**関連テーブルを取得するSQLをN回実行する。**|対象Entity取得時|
|4|LAZY|しない|**関連テーブルを取得するSQLをN回実行する。**|関連エンティティのフィールド参照時|
	
## 使うべきパターン
||fetchType|JPQLでJOIN FETCH実行|SQL実行結果|関連テーブルを取得するSQL実行タイミング|
|----|----|----|----|----|
|3|LAZY|する|**1回のSQLで対象テーブル＋関連テーブルを取得する。**|対象Entity取得時|