Як використовувати JOIN FETCH для вирішення проблеми N+1
JOIN FETCH — найпоширеніший спосіб вирішення проблеми N+1 в JPA. Він дозволяє завантажити пов'язані сутності в одному SQL-запиті замість N окремих запитів.
Огляд
JOIN FETCH — найпоширеніший спосіб вирішення проблеми N+1 в JPA. Він дозволяє завантажити пов’язані сутності в одному SQL-запиті замість N окремих запитів.
🟢 Junior Level
Що таке JOIN FETCH
JOIN FETCH — завантажує пов’язані сутності в одному запиті замість N+1.
// Проблема N+1:
List<Order> orders = em.createQuery("SELECT o FROM Order o", Order.class)
.getResultList(); // 1 запит
for (Order o : orders) {
o.getItems().size(); // N запитів
}
// Рішення з JOIN FETCH:
List<Order> orders = em.createQuery(
"SELECT DISTINCT o FROM Order o JOIN FETCH o.items", Order.class)
.getResultList(); // 1 запит з JOIN!
Навіщо DISTINCT
// JOIN може дублювати Order (по одному на кожен OrderItem)
// DISTINCT прибирає дублікати в пам'яті
SELECT DISTINCT o FROM Order o JOIN FETCH o.items
🟡 Middle Level
Один JOIN FETCH на запит
// ✅ Один JOIN FETCH
@Query("SELECT DISTINCT o FROM Order o JOIN FETCH o.items WHERE o.id = :id")
Order findByIdWithItems(@Param("id") Long id);
// ❌ Багато JOIN FETCH — Cartesian Product
@Query("SELECT o FROM Order o " +
"JOIN FETCH o.items " +
"JOIN FETCH o.payments " +
"JOIN FETCH o.shippings")
// Якщо 10 замовлень × 5 items × 3 payments × 2 shippings = 300 рядків!
Альтернатива — @BatchSize
// Замість кількох JOIN FETCH:
@Query("SELECT DISTINCT o FROM Order o JOIN FETCH o.user")
List<Order> findAllWithUser();
// + @BatchSize для items
@OneToMany(mappedBy = "order")
@BatchSize(size = 25)
private List<OrderItem> items;
Типові помилки
// ❌ Без DISTINCT — дублікати
@Query("SELECT o FROM Order o JOIN FETCH o.items")
List<Order> findAll(); // дублікати Order!
// ❌ JOIN FETCH з pagination
@Query("SELECT DISTINCT o FROM Order o JOIN FETCH o.items")
Page<Order> findAll(Pageable pageable); // ❌ може не працювати
// ✅ З окремим count query
@Query(value = "SELECT DISTINCT o FROM Order o JOIN FETCH o.items",
countQuery = "SELECT COUNT(o) FROM Order o")
Page<Order> findAll(Pageable pageable);
🔴 Senior Level
JOIN FETCH та pagination — проблема
Чому JOIN FETCH + pagination проблематична: Hibernate не може коректно застосувати LIMIT/OFFSET до запиту з JOIN — результат містить дублікати root-сутностей.
// Рішення 1: окремий count query
@Query(value = "SELECT DISTINCT o FROM Order o JOIN FETCH o.items",
countQuery = "SELECT COUNT(DISTINCT o) FROM Order o")
Page<Order> findAllWithPage(Pageable pageable);
// Рішення 2: два запити
@Query("SELECT o.id FROM Order o")
Page<Long> findOrderIds(Pageable pageable);
@Query("SELECT DISTINCT o FROM Order o JOIN FETCH o.items WHERE o.id IN :ids")
List<Order> findByIds(@Param("ids") List<Long> ids);
Best Practices
✅ Один JOIN FETCH на запит
✅ DISTINCT для усунення дублікатів
✅ @BatchSize для додаткових зв'язків
✅ EntityGraph для динамічного завантаження
✅ Різні методи для різних use cases
❌ Багато JOIN FETCH (Cartesian product)
❌ Без DISTINCT
❌ JOIN FETCH з pagination без countQuery
🎯 Шпаргалка для співбесіди
Обов’язково знати:
- JOIN FETCH завантажує пов’язані сутності в одному SQL-запиті — рішення N+1
- DISTINCT обов’язковий — прибирає дублікати root-об’єктів
- Один JOIN FETCH на запит — кілька → Cartesian product
- Для pagination з JOIN FETCH потрібен окремий countQuery
- Альтернатива для додаткових зв’язків — @BatchSize замість кількох JOIN FETCH
Пов’язані теми:
- [[1. Що таке проблема N+1 і як її вирішити]]
- [[6. Що робить анотація @BatchSize]]
- [[5. Які стратегії fetch існують в Hibernate]]
- [[29. Що таке projection в JPA]]