Какие стратегии 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]]