Питання 5 · Розділ 16

Які стратегії fetch існують в Hibernate

Fetch стратегії визначають як Hibernate завантажує пов'язані сутності з бази даних.

Мовні версії: English Russian Ukrainian

Огляд

Fetch стратегії визначають як Hibernate завантажує пов’язані сутності з бази даних.

FetchType (JPA) відповідає за «коли» — одразу чи при зверненні. FetchMode (Hibernate) — за «як» — одним запитом, окремими, пакетом. Розуміння стратегій fetch — ключ до оптимізації продуктивності та запобігання проблем типу N+1.


🟢 Junior Level

Основні стратегії

Hibernate надає 4 стратегії fetch:

  1. SELECT — окремий запит при зверненні до зв’язку (за замовчуванням для LAZY)
  2. JOIN — один запит з JOIN (за замовчуванням для EAGER)
  3. SUBSELECT — підзапит для колекції
  4. BATCH — пакетне завантаження кількох сутностей

Важливо: BATCH — це не FetchMode, а окремий механізм через @BatchSize. Реальна класифікація:

  • FetchMode (Hibernate-специфічний): SELECT, JOIN, SUBSELECT
  • Окремий механізм: @BatchSize (пакетне завантаження)
  • Динамічний: EntityGraph (JPA-стандарт)
// SELECT — окремий запит при зверненні
@OneToMany(fetch = FetchType.LAZY)
@Fetch(FetchMode.SELECT)
private List<OrderItem> items;

// JOIN — один запит з JOIN
@OneToMany(fetch = FetchType.EAGER)
@Fetch(FetchMode.JOIN)
private List<OrderItem> items;

// SUBSELECT — підзапит для колекції
@OneToMany(mappedBy = "order")
@Fetch(FetchMode.SUBSELECT)
private List<OrderItem> items;

Різниця між FetchType та FetchMode

  FetchType (JPA) @Fetch (Hibernate)
Стандарт JPA стандарт Hibernate специфіка
Значення LAZY, EAGER SELECT, JOIN, SUBSELECT
Коли Коли завантажувати Як завантажувати

🟡 Middle Level

Детальний опис FetchMode

SELECT

@Fetch(FetchMode.SELECT)
// Окремий SELECT для кожної сутності
// При завантаженні 10 Order → 10 SELECT для items
// Викликає N+1 проблему!

JOIN

@Fetch(FetchMode.JOIN)
// Один запит з JOIN
// SELECT o.*, i.* FROM orders o JOIN order_items i ON o.id = i.order_id

// Важливо: JOIN ігнорує FetchType.LAZY!
// Навіть якщо вказано LAZY, все одно завантажить одразу
// В Hibernate 5.x JOIN ігнорує LAZY. В Hibernate 6+ поведінку уточнено, але для гарантованого lazy використовуйте JOIN FETCH в JPQL.

SUBSELECT

@Fetch(FetchMode.SUBSELECT)
// Один підзапит для всієї колекції
// SELECT * FROM order_items WHERE order_id IN (SELECT id FROM orders)

// Корисно коли завантажено кілька батьківських сутностей
// SUBSELECT працює тільки якщо батьківські сутності були завантажені одним запитом.
// Якщо завантажували по одній — Hibernate fallback до SELECT.

Практичні приклади

@Entity
public class Order {

    // LAZY + SELECT = N+1 (погано)
    @OneToMany(mappedBy = "order", fetch = FetchType.LAZY)
    @Fetch(FetchMode.SELECT)
    private List<OrderItem> items;

    // LAZY + JOIN = завжди JOIN (ігнорує LAZY)
    @OneToMany(mappedBy = "order", fetch = FetchType.LAZY)
    @Fetch(FetchMode.JOIN)
    private List<OrderItem> items;

    // LAZY + SUBSELECT = один підзапит
    @OneToMany(mappedBy = "order", fetch = FetchType.LAZY)
    @Fetch(FetchMode.SUBSELECT)
    private List<OrderItem> items;
}

Типові помилки

// ❌ FetchMode.JOIN ігнорує LAZY
@Fetch(FetchMode.JOIN)
@OneToMany(fetch = FetchType.LAZY)
// Все одно завантажить одразу через JOIN

// ❌ Використання без розуміння наслідків
@Fetch(FetchMode.SUBSELECT)
@ManyToOne
// SUBSELECT має сенс тільки для колекцій

🔴 Senior Level

Внутрішня реалізація

При конфлікті FetchMode перевизначає FetchType. LAZY + JOIN = все одно JOIN. Це часта помилка.

FetchMode.JOIN:
- При завантаженні сутності одразу робить LEFT/INNER JOIN
- Повністю ігнорує FetchType.LAZY
- Результат: всі пов'язані сутності завантажені
- Підходить коли зв'язок потрібен в 100% випадків

FetchMode.SELECT:
- При першому зверненні до proxy
- Виконує SELECT WHERE foreign_key = ?
- Для кожної сутності — окремий запит
- Викликає N+1 проблему

FetchMode.SUBSELECT:
- Збирає всі ID завантажених сутностей
- Один запит: WHERE parent_id IN (id1, id2, ...)
- Ефективно для невеликих колекцій батьків

Глобальне налаштування

# application.yml
spring:
  jpa:
    properties:
      hibernate:
        # Глобальний batch size для всіх LAZY зв'язків
        default_batch_fetch_size: 50

        # Або через Hibernate анотації
        # @BatchSize(size = 50) на рівні класу/колекції

Просунуті стратегії

Batch fetching (рекомендований підхід)

@OneToMany(mappedBy = "order")
@BatchSize(size = 25)
private List<OrderItem> items;

// Замість 100 запитів → 4 запити (100/25)
// Hibernate: SELECT * FROM order_items WHERE order_id IN (?, ..., ?) -- 25 ID

Dynamic fetching через EntityGraph

// Статичний EntityGraph
@NamedEntityGraph(
    name = "Order.withItems",
    attributeNodes = @NamedAttributeNode("items")
)
@Entity
public class Order { }

// Використання
EntityGraph<Order> graph = entityManager.getEntityGraph("Order.withItems");
List<Order> orders = entityManager.createQuery("SELECT o FROM Order o", Order.class)
    .setHint("jakarta.persistence.fetchgraph", graph)
    .getResultList();

Порівняння підходів

Стратегія Запитів Гнучкість Коли використовувати
SELECT N+1 Ні Коли зв’язок не потрібен (LAZY за замовч.)
JOIN 1 Ні Зв’язок потрібен завжди
SUBSELECT 1 Ні Кілька батьків
BATCH N/batchSize Так Колекції
EntityGraph 1 Так Динамічне завантаження
JOIN FETCH 1 Так JPQL запити

Best Practices

✅ @BatchSize для колекцій (10-50)
✅ LAZY + JOIN FETCH в JPQL запитах
✅ EntityGraph для динамічного завантаження
✅ SUBSELECT для рідкісних випадків

❌ FetchMode.JOIN без причини (ігнорує LAZY)
❌ FetchMode.SELECT (викликає N+1)
❌ Змішування EAGER + JOIN
❌ Глобальний EAGER без необхідності

🎯 Шпаргалка для співбесіди

Обов’язково знати:

  • FetchType (JPA): LAZY, EAGER — відповідає за «коли» завантажувати
  • FetchMode (Hibernate): SELECT, JOIN, SUBSELECT — відповідає за «як» завантажувати
  • FetchMode.JOIN ігнорує LAZY — часта помилка
  • @BatchSize — окремий механізм, не FetchMode, завантажує пакетами
  • EntityGraph — динамічне завантаження, найгнучкіший підхід
  • При конфлікті FetchMode перевизначає FetchType

Часті уточнюючі запитання:

  • У чому різниця FetchType vs FetchMode? FetchType — JPA стандарт (коли), FetchMode — Hibernate специфіка (як)
  • Чому FetchMode.JOIN ігнорує LAZY? JOIN одразу робить JOIN в SQL, LAZY не може працювати
  • Що таке SUBSELECT? Один підзапит для всіх завантажених батьків: WHERE parent_id IN (SELECT id FROM parents)
  • Коли використовувати EntityGraph? Коли потрібно динамічно обирати що завантажувати в різних сценаріях

Червоні прапорці (НЕ говорити):

  • «FetchMode.JOIN + LAZY = ліниве завантаження» — JOIN ігнорує LAZY
  • «Використовую FetchMode.SELECT за замовчуванням» — викликає N+1
  • «@BatchSize це FetchMode» — це окремий механізм
  • «EAGER + JOIN — найкраща комбінація» — антипатерн

Пов’язані теми:

  • [[1. Що таке проблема N+1 і як її вирішити]]
  • [[2. У чому різниця між Lazy та Eager завантаженням]]
  • [[6. Що робить анотація @BatchSize]]
  • [[28. Як використовувати JOIN FETCH для вирішення проблеми N+1]]