Ответ
Проблема 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: Загружает связанные коллекции пачками, а не по одной.
Ответ 18+ 🔞
А, ну это же классика, ёпта! Сидишь такой, думаешь, что всё оптимизировал, а потом бац — и приложение ползёт, как улитка по стеклу, намазавшись маслом. А виной всему этот, блядь, N+1 запрос, хитрая жопа такая.
Представь себе, сука, ты пишешь код, который вроде бы логичный. Хочешь получить список авторов и посмотреть, сколько у каждого книг. Ну, типа, SELECT * FROM Author. Один запрос, всё окей, волнение ебать.
А потом ты в цикле начинаешь к каждой книжной коллекции обращаться. И вот тут-то, блядь, и начинается пиздец. Твоя ORM, эта мартышлюшка, для каждого автора по отдельности лезет в базу: SELECT * FROM Book WHERE author_id = ?. Получается, если авторов 100 (N), то запросов будет 1 + 100. Вот тебе и N+1, овердохуища лишней работы!
Код, который это вызовет, выглядит вроде бы невинно, а на деле — диверсия:
// Запрос 1: всех авторов взяли
List<Author> authors = entityManager
.createQuery("SELECT a FROM Author a", Author.class)
.getResultList();
// А тут для каждого — свой отдельный запрос в БД. Пизда рулю.
for (Author author : authors) {
System.out.println(author.getBooks().size()); // Ой, бля, а книг-то нет, надо сходить за ними!
}
Решение, блядь? Да не городить же сто хуёв, а сразу всё нужное за раз выгрести! Спасает JOIN FETCH, который как добрый самаритянин, одним махом всё приносит.
// Всё! Один здоровенный запрос, и авторы, и книги у них в кармане.
List<Author> authors = entityManager
.createQuery(
"SELECT DISTINCT a FROM Author a LEFT JOIN FETCH a.books", // FETCH — это ключевое слово, ёпта!
Author.class
)
.getResultList();
// Теперь author.getBooks() уже загружены, можно хоть обосраться от счастья, новых запросов не будет.
Ну а если JOIN FETCH не по душе, есть и другие варианты, конечно:
- Entity Graphs — чтоб не писать JOIN'ы руками, типа как для грамотных.
@BatchSize— эта штука не одним махом грузит, а пачками. Типа, не по одной книге таскать, а сразу по 10-20. Тоже норм, ноJOIN FETCHобычно быстрее, если тебе прямо все данные сразу нужны.
Короче, суть в чём: не заставляй базу дергаться на каждый чих. Бери всё, что нужно для задачи, одним здоровенным куском. И будет тебе счастье, а не тормоза.