Что такое проблема N+1 в Hibernate?

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

Ответ

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

Как возникает: При ленивой (LAZY) загрузции связанных сущностей Hibernate сначала выполняет запрос для родительских объектов, а затем, при обращении к дочерней коллекции для каждого родителя, генерирует отдельный запрос.

Пример проблемы:

@Entity
public class Author {
    @Id
    private Long id;
    @OneToMany(mappedBy = "author", fetch = FetchType.LAZY)
    private List<Book> books;
}

// HQL-запрос (1 запрос для авторов)
List<Author> authors = session.createQuery("FROM Author", Author.class).list();

// При обходе для каждого автора выполняется отдельный запрос для книг (N запросов)
for (Author author : authors) {
    System.out.println(author.getBooks().size()); // Триггер N+1!
}
// ИТОГО: 1 (авторы) + N (книги для каждого автора) запросов.

Решения:

  1. JOIN FETCH в JPQL/HQL:
    String jpql = "SELECT DISTINCT a FROM Author a LEFT JOIN FETCH a.books";
    List<Author> authors = session.createQuery(jpql, Author.class).getResultList();
  2. @EntityGraph (JPA 2.1):
    @EntityGraph(attributePaths = {"books"})
    @Query("SELECT a FROM Author a")
    List<Author> findAllWithBooks();
  3. Пакетная загрузка (@BatchSize):
    @OneToMany(mappedBy = "author")
    @BatchSize(size = 10)
    private List<Book> books;
    // Hibernate загрузит книги для авторов пачками (например, по 10)

Критерий выбора: JOIN FETCH для небольшого известного набора данных, @BatchSize для больших или вложенных графов.