【Spring Data JPA】N+1問題

はじめに

お世話にになります、hosochinです
Spring Data JPAでよくあるN+1問題の解決方法についてメモします

JPAのN+1問題

N+1問題とは、意図しないSQL文が大量に(取得するレコードの数だけ)発行されてしまう問題です
この問題はO/R MapperによるSQL文の自動生成が原因で、たとえば以下のような現象になります

  1. 対象テーブルからN個のレコードを取得するSELECT文を1回発行する
  2. 1で取得したN個のレコードの関連データを取得するSELECT文を1回ずつ発行する(N回発行される)

N+1問題を発生させてみる

DB

以下のようなテーブルにレコードが入っているとします

  • userテーブル
user_id (PK)user_name
1Taro
2Jiro
3Saburo
  • teamテーブル
  • teamテーブル.user_iduserテーブル.user_id と多対1の関係
team_id (PK)team_nameuser_id (PK)
1Aチーム1
1Aチーム2
2Bチーム3

実装

Lombokいれてます

  • userテーブルのEntity
@Entity
@Table(name="user")
@Data
public class UserEntity {

    @Id
    @Column(name = "user_id")
    private String userId;

    private String userName;

}
  • teamテーブルのEntity
@Entity
@Table(name="team")
@IdClass(value=TeamKey.class)
@Data
public class JoinTeamEntity implements Serializable {

    @Id
    @Column(name = "user_id")
    private String teamId;

    private String teamName;

    @Id
    private String userId;

    // userテーブルと結合するキー
    @ManyToOne
    @JoinColumn(name = "user_id", insertable = false, updatable = false)
    private UserEntity user;
}
  • teamテーブルの複合主キークラス
@Data
public class TeamKey implements Serializable {

    @Id
    private String teamId;

    @Id
    @Column(name = "user_id")
    private String userId;

}
  • repositoryクラス
public interface TeamRepository extends JpaRepository<JoinTeamEntity, TeamKey> {}

findAll()してみる

application.propertiesのspring.jpa.show-sqlをtrueにすると発行されたクエリがログ出力されるようになります
こいつをtrueにしてから動かします

  • 実行されたクエリ(見やすいように少し整形してます)
select user_id, team_id, team_name from team;
select user_id, user_name from user where user_id='1';
select user_id, user_name from user where user_id='2';
select user_id, user_name from user where user_id='3';

最初にチームテーブルをselectして、そこで取得したuser_idの数だけuserテーブルにselectする…N+1問題が発生しています

解決方法

JPQLでJOIN FETCHを使って解決するという方法があります
クエリのJOIN のあとに FETCH をつけるだけです

  • repositoryクラス
public interface TeamRepository extends JpaRepository<JoinTeamEntity, TeamKey> {
        @Query("SELECT t FROM JoinTeamEntity t LEFT JOIN FETCH t.user")
        List<JoinTeamEntity> findAll();
}
  • 実行されるクエリ
select
  team.user_id,
  team.team_id,
  team.team_name,
  user.user_id,
  user.user_name
from
  team team
left outer join user user on team.user_id=user.user_id;

👍