Ответ
Проблема 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: Загружает связанные коллекции пачками, а не по одной.