Как я решал проблему N+1 в Hibernate

Разрабатывал Spring Boot-приложение с сущностями Task и TaskDetail (одна задача → много деталей). Столкнулся с классической проблемой N+1 запросов, когда пытался загружать данные.

Как проявлялась проблема

Было 2 сценария: 1. Простой список задач (без деталей) - для табличного отображения 2. Полная задача со всеми деталями - для детальной страницы

Наивная реализация: @Entity public class Task { @Id private Long id;

@OneToMany(mappedBy = "task", fetch = FetchType.LAZY) private List<TaskDetail> details; }

Когда делал так: List<Task> tasks = taskRepository.findAll(); tasks.forEach(task -> { System.out.println(task.getDetails().size()); });

В логах Hibernate (spring.jpa.show-sql=true) видел: Hibernate: select t1_0.id from task t1_0 Hibernate: select d1_0.task_id from task_detail d1_0 where d1_0.task_id=? Hibernate: select d1_0.task_id from task_detail d1_0 where d1_0.task_id=? ... и так для каждой задачи

Для 100 задач → 101 запрос

Как исправил (3 способа)

1. @EntityGraph - точечная жадная загрузка

После изучения проблемы пришел к выводу:

  • Все ассоциации всегда должны быть LAZY
  • Жадная загрузка только через @EntityGraph когда действительно нужно

public interface TaskRepository extends JpaRepository<Task, Long> {

List<Task> findAll();

@EntityGraph(attributePaths = "details") List<Task> findAllWithDetails(); }

Как работает @EntityGraph:

  • Превращает LAZY-ассоциации в FETCH для конкретного запроса
  • Генерирует LEFT OUTER JOIN
  • Не влияет на другие методы репозитория

Главные выводы

1. Все ассоциации делаем LAZY по умолчанию 2. Жадная загрузка только через @EntityGraph когда точно нужно 3. JOIN FETCH для сложных кастомных запросов 4. BatchSize как компромисс для ленивой загрузки