Проблема N+1
Проблема N+1 — самая частая ловушка производительности в Hibernate. Вы загружаете список из N статей одним запросом, потом обращаетесь к автору каждой — и каждый ленивый автор загружается своим запросом. Один запрос на список плюс N на подробности. На трёх статьях это почти незаметно, на 500 — 501 запрос на одну страницу.
Воспроизводим
Тестовые данные — класс Seed из прошлого урока: три статьи Анны, Олега и Марии.
// src/main/java/com/example/blog/Main.java
Seed.run(emf);
emf.runInTransaction(em -> {
List<Post> posts = em.createQuery("select p from Post p order by p.id", Post.class)
.getResultList();
for (Post post : posts) {
IO.println(post.getTitle() + ", автор: " + post.getAuthor().getName());
}
});Hibernate: select p1_0.id,p1_0.author_id,p1_0.title,p1_0.views from Post p1_0 order by p1_0.id
Hibernate: select a1_0.id,a1_0.name from Author a1_0 where a1_0.id=?
Привет, Hibernate, автор: Анна
Hibernate: select a1_0.id,a1_0.name from Author a1_0 where a1_0.id=?
Типы record в Java 25, автор: Олег
Hibernate: select a1_0.id,a1_0.name from Author a1_0 where a1_0.id=?
Ленивая загрузка, автор: МарияЧетыре запроса на три статьи. Код выглядит безобидно — проблема видна только в журнале SQL.
Способ 1: join fetch
join fetch в запросе велит Hibernate загрузить связь вместе с основной сущностью:
// src/main/java/com/example/blog/Main.java
List<Post> posts = em.createQuery(
"select p from Post p join fetch p.author order by p.id", Post.class)
.getResultList();Hibernate: select p1_0.id,a1_0.id,a1_0.name,p1_0.title,p1_0.views from Post p1_0 join Author a1_0 on a1_0.id=p1_0.author_id order by p1_0.id
Привет, Hibernate, автор: Анна
Типы record в Java 25, автор: Олег
Ленивая загрузка, автор: МарияОдин запрос. Учтите, что join fetch — внутреннее соединение: статьи без автора пропали бы. Для необязательных связей пишите left join fetch.
Способ 2: граф сущности
Граф сущности (entity graph) перечисляет связи для загрузки отдельно от текста запроса. Один и тот же запрос можно использовать с разными графами:
// src/main/java/com/example/blog/Main.java
EntityGraph<Post> graph = em.createEntityGraph(Post.class);
graph.addAttributeNode("author");
graph.addAttributeNode("comments");
List<Post> posts = em.createQuery("select p from Post p order by p.id", Post.class)
.setHint("jakarta.persistence.fetchgraph", graph)
.getResultList();Hibernate: select p1_0.id,a1_0.id,a1_0.name,c1_0.post_id,c1_0.id,c1_0.text,p1_0.title,p1_0.views from Post p1_0 left join Author a1_0 on a1_0.id=p1_0.author_id left join Comment c1_0 on p1_0.id=c1_0.post_id order by p1_0.id
Привет, Hibernate, автор: Анна, комментариев: 2
Типы record в Java 25, автор: Олег, комментариев: 1
Ленивая загрузка, автор: Мария, комментариев: 0Авторы и комментарии пришли одним запросом. У первой статьи два комментария, поэтому база вернула её дважды, но Hibernate сам убрал повторы — в списке три статьи.
Способ 3: пакетная загрузка
Если менять запросы нельзя, добавьте свойство в persistence.xml:
<!-- src/main/resources/META-INF/persistence.xml -->
<property name="hibernate.default_batch_fetch_size" value="20"/>Теперь ленивые связи загружаются группами:
Hibernate: select p1_0.id,p1_0.author_id,p1_0.title,p1_0.views from Post p1_0 order by p1_0.id
Hibernate: select a1_0.id,a1_0.name from Author a1_0 where a1_0.id in (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)Два запроса вместо N+1, и код трогать не пришлось. В Spring Boot то же свойство записывается как spring.jpa.properties.hibernate.default_batch_fetch_size=20.
Частая ошибка
Перевод связи на FetchType.EAGER не лечит N+1 в запросах. С @ManyToOne(fetch = FetchType.EAGER) тот же JPQL-запрос всё равно загружает каждого автора отдельно — даже если код к ним не обращается:
Hibernate: select p1_0.id,p1_0.author_id,p1_0.title,p1_0.views from Post p1_0 order by p1_0.id
Hibernate: select a1_0.id,a1_0.name from Author a1_0 where a1_0.id=?
Hibernate: select a1_0.id,a1_0.name from Author a1_0 where a1_0.id=?
Hibernate: select a1_0.id,a1_0.name from Author a1_0 where a1_0.id=?Оставляйте связи ленивыми и в каждом запросе указывайте, что загрузить.