TGViewer
Spring АйО Spring АйО @spring_aio · 11K subscribers
Post #184 4.37K
😡 Spring Data JPA: Проблема N+1 и один из способов её решения

Одной из часто обсуждаемых тем на собеседованиях по Spring Data JPA является проблема N+1 и возможные способы её решения. Многие отвечают, что её можно решить с помощью join fetch или @EntityGraph. Однако это не всегда так.

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


@Query("""
select distinct a
from Author a
left join fetch a.books
""")
List<Author> findAllFetch();

@EntityGraph(attributePaths = {"books"})
@Query("""
select distinct a
from Author a
""")
List<Author> findAllFetch();


Но что если нужно выбрать только часть данных, например, постранично? Тогда запросы будут выглядеть так:


@Query("""
select a
from Author a
left join fetch a.books
""")
Slice<Author> findAllFetch(Pageable page);

@EntityGraph(attributePaths = {"books"})
@Query("""
select distinct a
from Author a
""")
List<Author> findAllFetch(Pageable page);


Проблема в том, что когда мы используем Pageable, Hibernate сначала загрузит все записи в память, а затем отсортирует их. Это может привести к OutOfMemoryError и/или серьёзной деградации производительности.

В защиту Hibernate отметим, что столь неоптимизированное действие он не станет выполнять "втихую" и выведет следующий warning в логах:


WARN: HHH000104: firstResult/maxResults specified with collection fetch; applying in memory!


Более того, указав следующее свойство в application.properties/yaml, Hibernate и вовсе начнёт падать с JpaSystemException:


spring.jpa.properties.hibernate.query.fail_on_pagination_over_collection_fetch=true


Как же на самом деле стоит решать проблему N+1?

Правильное решение — использование аннотации @BatchSize:


@Entity
public class Owner extends Person {
@OneToMany(cascade = CascadeType.ALL, mappedBy = "owner")
@BatchSize(size = 50)
private List<Pet> pets = new ArrayList<>();
}


Если коллекций много, можно настроить глобальный размер выборки в файле application.properties/yaml:


spring.jpa.properties.hibernate.default_batch_fetch_size=50


С использованием @BatchSize или глобального свойства запросы можно переписать следующим образом:


@Query("""
select a
from Author a
""")
Slice<Author> findAllFetch(Pageable page);

List<Author> findAll(Pageable pageable);


Вывод

Используйте @BatchSize или spring.jpa.properties.hibernate.default_batch_fetch_size, чтобы избежать проблемы N+1 без загрузки избыточного количества данных в память.

Стоит отметить, что используя этот подход важно помнить про необходимость обращения к элементам связанной коллекции в условиях наличия открытой транзакции, чтобы Hibernate смог загрузить необходимые данные. Несмотря на то, что spring.jpa.open-in-view=true позволяет нам быть уверенными в наличии открытой транзакции, он не является рекомендуемым.

Для более глубокого понимания оптимизации запросов рекомендуем ознакомиться с докладом экспертов сообщества Spring АйО – Ильи и Фёдора Сазоновых, с которым они выступали на последнем JPoint.

#SpringTips #JPA #DB
  • ❤ 40
  • 👍 19
  • 🔥 15
  • 👎 1
More from @spring_aio
  1. Oct 11, 2026🎬 Записи докладов с JPoint 2026 уже в открытом доступе JUG Ru Group выложили записи с про…
  2. Oct 9, 2026👩‍💻 Kotlin получил почти настоящие статики Начиная с Kotlin 2.5.0-Beta1 можно эксперимен…
  3. Oct 8, 2026🍃 Spring Boot ломает совместимость, G1 тормозит микросервисы, Java долго релизит фичи | S…
  4. Oct 6, 2026👩‍💻 Spring Config Import без магии У наших друзей из Axelix вышла статья о том, как Spri…
  5. Oct 5, 2026🥲 В Java 27 TLS научился переживать квантовую атаку При установке HTTPS-соединения клиент…
  6. Oct 3, 2026⚡️⚡️Строки кода стремятся подешеветь до цены токена. Что остается ценным? Про это — трек «…
Threads Profile ViewerView any public Threads profile without an account.Open ThreadLook →Writing with AI? Make it sound human.Metric37 rewrites AI drafts so they read naturally. Free AI detector, 1,500 words free.Try Metric37 →