GOAL
JAVAでDBMSを操作するためにJPAを使用する際に頻繁に発生する「N+1問題」の発生原因及び解決策の基本を知る。
目次
N+1問題とは
以下のようなEntity, Repositoryがあるとしましょう。
@Entity
public class Teacher {
@Id
@GeneratedValue
private long id;
@OneToMany
private List<Student> students = Lists.newArrayList();
}
@Entity
public class Student {
@Id
@GeneratedValue
private long id;
@ManyToOne
private Teacher teacher;
}
public interface TeacherRepository extends JpaRepository<Long, Master> {
//JpaRepositoryを継承しているため、デフォルトのメソッドは入っている
}
TeacherRepository.findAll()を実行すると以下のようなクエリが発行されることを期待するでしょう。
SELECT * FROM Teacher
LEFT JOIN STUDENT
ON STUDENT.TEACHER_ID = TEACHER.ID
でも実際のクエリを確認すると以下のように
TEACHERを取得するクエリ → 1回
各TEACHERのSTUDENTを取得するクエリ → N回
のクエリが発行され、「N+1問題」が発生してしまいます。
TEACHERの数が多い場合、パフォマンスに致命的な影響を与えることになるでしょう。
SELECT * FROM TEACHER //1回目のクエリ
SELECT * FROM STUDENT WHERE TEACHER_ID = 0 //2目のクエリ
SELECT * FROM STUDENT WHERE TEACHER_ID = 1 //3目のクエリ
SELECT * FROM STUDENT WHERE TEACHER_ID = 2 //4目のクエリ
SELECT * FROM STUDENT WHERE TEACHER_ID = 3 //5目のクエリ
・・・・
SELECT * FROM STUDENT WHERE TEACHER_ID = N //N+1目のクエリ
原因
JPAから提供しているメソッドはDBにダイレクトにクエリを発行するのではなく、JPQLというオブジェクト指向クエリ言語を生成・実行してからJPAがこれを分析し、SQLを生成してクエリをDBに発行する動きになっているからです。
上記の例の場合の詳細な動きは以下のようになる。
- select o from Order o JPQLを分析し、 select * from Teacher SQLを1回発行する
- SQLの結果に基づいてTeacherオブジェクトを生成する。
- TeacherのStudentオブジェクトを取得するために、SELECT * FROM STUDENT WHERE TEACHER_ID=? SQLをN回発行する
解決策
join fetch適用
カスタムレポジトリを作成してJPQLにてfetch明記すれば問題は簡単に解決できます。
select m from Teacher m join fetch m.students
上記のように明記すると、以下のようなSQLクエリが発行されるようになります。
SELECT * FROM TEACHER
LEFT JOIN STUDENT
ON STUDENT.TEACHER_ID = TEACHER_ID
しかし、join fetchをすることにより、以下のようなデメリットが生じます。
・一発で全てのオブジェクトを取得するため、ページング単位で取得ができなくなり、ページング機能の実現が難しい。
・不要なクエリが増える。
#最後に
当記事ではJPAの「N+1問題」の基本を紹介しているため、
LazyFetch、EagerFetchのようなFetch戦略に関して詳しく説明していないが、「N+1問題」をきちんと理解するためには必須知識であるためFetch戦略は他の記事を参考にしてください。