Encodera

Спасибо, что читаете!

Пожертвуйте сайту, мы стараемся ради вас.

Оглавление

Проблема 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=?

Оставляйте связи ленивыми и в каждом запросе указывайте, что загрузить.

Спасибо, что читаете!

Пожертвуйте сайту, мы стараемся ради вас.