Вопрос 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]]