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?

【Java/JPA】N+1問題の概要と解決方法

Posted at

はじめに

N+1問題とは、主にデータベースからのデータ取得時に起こる不要に多くのクエリが発生する問題のことです。たとえば、あるテーブルAからN件のデータを取得し、その各レコードに関連するテーブルBから追加の情報を個別に取得するような場合、合計でN+1回のクエリ(最初の1回 + N回)が実行されてしまうケースを指します。Java(特にJPA/Hibernate)でよく起こるN+1問題の例と、その解決方法を示します。

1. 前提:エンティティ定義

import javax.persistence.*;
import java.util.List;

@Entity
@Table(name = "users")
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    // デフォルトでは Lazy Fetch
    @OneToMany(mappedBy = "user", fetch = FetchType.LAZY)
    private List<Post> posts;

    // getter / setter
    public Long getId() { return id; }
    public String getName() { return name; }
    public List<Post> getPosts() { return posts; }
    public void setId(Long id) { this.id = id; }
    public void setName(String name) { this.name = name; }
    public void setPosts(List<Post> posts) { this.posts = posts; }
}
import javax.persistence.*;

@Entity
@Table(name = "posts")
public class Post {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String title;

    // 多対1でUserを参照
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "user_id")
    private User user;

    // getter / setter
    public Long getId() { return id; }
    public String getTitle() { return title; }
    public User getUser() { return user; }
    public void setId(Long id) { this.id = id; }
    public void setTitle(String title) { this.title = title; }
    public void setUser(User user) { this.user = user; }
}
  • User エンティティは複数の Post を持ち、@OneToMany(mappedBy = "user") で関連付けています。
  • どちらも fetch = FetchType.LAZY になっているため、実際に getPosts() を呼び出したタイミングで DB にクエリを投げます。

2. N+1問題の例

import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import java.util.List;

@Service
public class UserService {

    @PersistenceContext
    private EntityManager em;

    public List<User> findAllUsersNaive() {
        // まず全Userを取得 (ここでは1回のクエリ)
        List<User> users = em.createQuery("SELECT u FROM User u", User.class)
                             .getResultList();

        // 全ユーザーのpostsにアクセスしたタイミングで、各ユーザーごとにSQL発行 (N回)
        for (User user : users) {
            List<Post> posts = user.getPosts(); 
            // ここで LAZY ロードにより 「SELECT * FROM posts WHERE user_id=?」 が発行される
            // ユーザー数がN人なら、N回クエリが走る
        }

        return users;
    }
}
  1. SELECT u FROM User u → 1回 のクエリ
  2. 取得したUserがN件ある場合、user.getPosts() を呼ぶたびに SELECT * FROM posts WHERE user_id = ? → N回 のクエリ
  3. 合計 N+1回 のクエリが発行される

このようにユーザー数が増えるほどクエリが指数的に増加し、アプリケーション性能が著しく低下します。

3. 解決策:Fetch Join を使って一括取得

N+1問題を回避する方法の1つが、JPQLでJOIN FETCHを使う(または Spring Data JPA のメソッド構造を使う)や、@EntityGraph を用いて関連テーブルを Eager Loading(事前読み込み)する方法です。

3.1 JPQL の Fetch Join 例

@Service
public class UserService {

    @PersistenceContext
    private EntityManager em;

    public List<User> findAllUsersWithPosts() {
        // Userと紐づくPostをJOIN FETCHでまとめて一括取得
        // N+1問題を回避し、クエリは多くて2回程度 (実際には1回)
        List<User> users = em.createQuery(
            "SELECT u FROM User u JOIN FETCH u.posts",
            User.class
        ).getResultList();

        // forループで user.getPosts() を呼んでも、すでに取得済み
        for (User user : users) {
            List<Post> posts = user.getPosts(); 
            // 追加のSQLクエリは発行されない
        }

        return users;
    }
}
  • JOIN FETCH u.posts により、ユーザーと投稿を 1回のクエリ でまとめて取得します。
  • これにより、getPosts() を呼び出しても新しいクエリは発行されず、N+1 を防げます。

3.2 Spring Data JPA の @EntityGraph

@Repository
public interface UserRepository extends JpaRepository<User, Long> {

    @EntityGraph(attributePaths = "posts")
    @Query("SELECT u FROM User u")
    List<User> findAllWithPosts();
}
  • @EntityGraph(attributePaths = "posts") を使うと、内部的に フェッチジョイン を使って取得してくれます。
  • コードをよりシンプルに書きたい場合に有効。

4. まとめ

4-1. N+1問題

  • ユーザー一覧を取得後、各ユーザーの紐づく投稿を個別に取得するため合計 N+1 回のクエリが走る状態を指す
  • パフォーマンスが大きく低下し、スケールしなくなる

4-2. 解決策

  • フェッチジョイン(JOIN FETCH)でまとめて取得
  • @EntityGraph などで Eager Loading
  • バッチ読み込み(IN句でまとめて取得)など
  • DBスキーマやアプリ設計を見直す

4-3. ポイント

  • Java(JPA/Hibernate)ではデフォルト LAZY フェッチが多く、無意識に getXxx() を呼ぶとクエリが発行される
  • 1対多 のリレーションで N+1 は起こりやすい
  • 大量データを扱う場面では特に注意し、SQL発行回数をログなどで確認する習慣を持つ

N+1問題 はアプリケーション開発において非常に一般的なパフォーマンス問題ですが、適切に フェッチジョイン や Eager Loading を使うことで解決できます。Java(JPA/Hibernate)の場合はJPQLの JOIN FETCH や Spring Data JPA の @EntityGraph を活用して、一括取得を行いましょう。

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?