В чём заключается проблема N+1 в Hibernate и как её решить?

«В чём заключается проблема N+1 в Hibernate и как её решить?» — вопрос из категории Hibernate, который задают на 10% собеседований Java Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

Проблема N+1 — это антипаттерн производительности, когда для загрузки одной сущности и связанной с ней коллекции выполняется 1 запрос для родительских записей и N отдельных запросов для дочерних (по одному на каждую родительскую запись).

Пример возникновения (с ленивой загрузкой FetchType.LAZY):

@Entity
public class Author {
    @Id
    @GeneratedValue
    private Long id;
    private String name;

    @OneToMany(mappedBy = "author", fetch = FetchType.LAZY)
    private List<Book> books; // Ленивая коллекция
}

@Entity
public class Book {
    @Id
    @GeneratedValue
    private Long id;
    private String title;

    @ManyToOne
    private Author author;
}

Проблемный код, вызывающий N+1 запросов:

// 1-й запрос: SELECT * FROM Author
List<Author> authors = entityManager
    .createQuery("SELECT a FROM Author a", Author.class)
    .getResultList();

// Для каждого автора (N раз) выполняется отдельный запрос:
// SELECT * FROM Book WHERE author_id = ?
for (Author author : authors) {
    // Обращение к коллекции инициирует её загрузку
    System.out.println(author.getBooks().size());
}

Решение: Использовать JOIN FETCH в JPQL.

// Один запрос с JOIN, который сразу загружает и авторов, и их книги
List<Author> authors = entityManager
    .createQuery(
        "SELECT DISTINCT a FROM Author a LEFT JOIN FETCH a.books",
        Author.class
    )
    .getResultList();
// Дальнейший доступ к author.getBooks() не вызывает дополнительных запросов к БД

Альтернативные решения:

  • Entity Graphs: Более типобезопасный и декларативный способ.
  • @BatchSize: Загружает связанные коллекции пачками, а не по одной.