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

Ответ

Проблема 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 для больших или вложенных графов.

Ответ 18+ 🔞

А, слушай, смотри, есть такая классическая засада, которая всех новичков на первом же повороте ебёт. Называется Проблема N+1. Это когда твой ORM, в нашем случае — Хибер, начинает вести себя как полный распиздяй и вместо одного умного запроса генерит овердохуища мелких.

В чём суть, блядь? Представь: ты грузишь кучу родительских сущностей (например, авторов) одним запросом. Это раз. А потом, когда ты в коде начинаешь у каждого автора спрашивать его книги (коллекцию, которая загружается лениво, LAZY), Хибер для КАЖДОГО, блядь, автора лезет в базу отдельным запросом. Итого: 1 (запрос за авторами) + N (запросов за книгами для каждого автора). Вот тебе и N+1, ебать мои старые костыли. Производительность летит в пизду.

Как это выглядит в жизни, на примере:

@Entity
public class Author {
    @Id
    private Long id;
    @OneToMany(mappedBy = "author", fetch = FetchType.LAZY) // Ленивая загрузка, сука!
    private List<Book> books;
}

// Сделали один запрос за всеми авторами
List<Author> authors = session.createQuery("FROM Author", Author.class).list();

// А вот тут начинается пиздец
for (Author author : authors) {
    // На каждой итерации Хибер, видя, что books не загружены, пиздует в базу за книгами именно этого автора
    System.out.println(author.getBooks().size()); // Триггер N+1, ёпта!
}
// ИТОГО: 1 (авторы) + N (книги для каждого автора) запросов. Удивление пиздец.

Как с этим бороться, чтобы не быть лохом:

  1. JOIN FETCH в JPQL/HQL — классика жанра. Просто говоришь Хиберу сразу: «Мужик, загружай всё вместе, не тупи». Он делает один жирный JOIN и возвращает всё разом.

    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) — для больших и сложных графов. Вешаешь аннотацию на коллекцию. Хибер перестаёт дрочиться по одному запросу на автора, а грузит книги пачками, например, сразу для 10 авторов одним запросом. Экономия — огонь.

    @OneToMany(mappedBy = "author")
    @BatchSize(size = 10) // «Загружай пачками по десять штук, не мелочись»
    private List<Book> books;

Критерий выбора, чтобы не облажаться: Если знаешь, что данных немного и структура простая — JOIN FETCH, быстро и понятно. Если граф объектов большой, вложенный, или не всегда нужны все связи — @BatchSize спасёт от перегрузки памяти. В общем, думай головой, а не надейся на авось.