Ответ
Проблема 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 (книги для каждого автора) запросов.
Решения:
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();@EntityGraph(JPA 2.1):@EntityGraph(attributePaths = {"books"}) @Query("SELECT a FROM Author a") List<Author> findAllWithBooks();- Пакетная загрузка (
@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 (книги для каждого автора) запросов. Удивление пиздец.
Как с этим бороться, чтобы не быть лохом:
-
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(); // Всё, теперь у каждого автора книги уже на руках, дополнительных запросов — ноль ебать. -
@EntityGraph(JPA 2.1) — более элегантный способ. Аннотацией прямо на методе репозитория указываешь, что хочешь подтянуть.@EntityGraph(attributePaths = {"books"}) // «Эй, чувак, не забудь про книги!» @Query("SELECT a FROM Author a") List<Author> findAllWithBooks(); -
Пакетная загрузка (
@BatchSize) — для больших и сложных графов. Вешаешь аннотацию на коллекцию. Хибер перестаёт дрочиться по одному запросу на автора, а грузит книги пачками, например, сразу для 10 авторов одним запросом. Экономия — огонь.@OneToMany(mappedBy = "author") @BatchSize(size = 10) // «Загружай пачками по десять штук, не мелочись» private List<Book> books;
Критерий выбора, чтобы не облажаться:
Если знаешь, что данных немного и структура простая — JOIN FETCH, быстро и понятно. Если граф объектов большой, вложенный, или не всегда нужны все связи — @BatchSize спасёт от перегрузки памяти. В общем, думай головой, а не надейся на авось.