Одной из часто обсуждаемых тем на собеседованиях по 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