Які стратегії fetch існують в Hibernate
Fetch стратегії визначають як Hibernate завантажує пов'язані сутності з бази даних.
Огляд
Fetch стратегії визначають як Hibernate завантажує пов’язані сутності з бази даних.
FetchType (JPA) відповідає за «коли» — одразу чи при зверненні. FetchMode (Hibernate) — за «як» — одним запитом, окремими, пакетом. Розуміння стратегій fetch — ключ до оптимізації продуктивності та запобігання проблем типу N+1.
🟢 Junior Level
Основні стратегії
Hibernate надає 4 стратегії fetch:
- SELECT — окремий запит при зверненні до зв’язку (за замовчуванням для LAZY)
- JOIN — один запит з JOIN (за замовчуванням для EAGER)
- SUBSELECT — підзапит для колекції
- 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]]