Вопрос 6 · Раздел 16

Что делает аннотация @BatchSize

@BatchSize — одна из самых полезных аннотаций Hibernate для борьбы с проблемой N+1. Она позволяет загружать связанные сущности пакетами (batch), значительно сокращая количество...

Версии по языкам: English Russian Ukrainian

Обзор

@BatchSize — одна из самых полезных аннотаций Hibernate для борьбы с проблемой N+1. Она позволяет загружать связанные сущности пакетами (batch), значительно сокращая количество SQL-запросов.


🟢 Junior Level

Что такое @BatchSize

Аннотация @BatchSize заставляет Hibernate загружать связанные сущности пакетами вместо выполнения отдельного запроса для каждой сущности.

// Без @BatchSize — N запросов
for (Order order : orders) {
    order.getItems().size();
    // SELECT * FROM order_items WHERE order_id = ?  (для каждого Order)
}
// 100 заказов → 100 запросов

// С @BatchSize(size = 10) — N/10 запросов
@Entity
public class Order {
    @OneToMany(mappedBy = "order")
    @BatchSize(size = 10)
    private List<OrderItem> items;
}

// Hibernate: SELECT * FROM order_items WHERE order_id IN (?,?,?,?,?,?,?,?,?,?)
// 100 заказов → 10 запросов (по 10 в каждом)

IN clause — часть SQL-запроса WHERE id IN (…). Базы данных лимитируют количество параметров. Prepared statement — предварительно скомпилированный SQL-запрос.

Как это работает

1. Hibernate собирает ID сущностей, для которых нужно загрузить связь
2. Вместо N отдельных запросов — формирует один запрос с IN (id1, id2, ...)
3. Размер пакета = значение @BatchSize
4. Количество запросов = N / batchSize

Где можно использовать

// На коллекции
@OneToMany(mappedBy = "order")
@BatchSize(size = 25)
private List<OrderItem> items;

// На классе (для загрузки нескольких сущностей по ID)
@Entity
@BatchSize(size = 50)
public class User { }

// При загрузке
List<User> users = session.createQuery("FROM User", User.class)
    .setHint(AvailableHints.HINT_SPEC_FETCH_SIZE, 50)
    .getResultList();
// Это другой механизм: fetch size контролирует сколько строк JDBC-драйвер
// выбирает за раз из курсора. @BatchSize контролирует сколько ID
// подставляется в IN-clause. Не путайте!

Когда @BatchSize бесполезен

  1. Загружается одна сущность — batch не нужен
  2. Используется JOIN FETCH — данные уже загружены
  3. Коллекция маленькая (1-3 элемента) — overhead batch > польза

🟡 Middle Level

Выбор оптимального размера

// ❌ Слишком маленький размер
@BatchSize(size = 2)  // почти не помогает, 50 запросов вместо 100

// ✅ Оптимальный диапазон
@BatchSize(size = 10)  // минимум
@BatchSize(size = 25)  // обычно хорошо
@BatchSize(size = 50)  // максимум (больше может быть проблема с IN clause)

Глобальная настройка

# application.yml — глобальный размер батча для всех
spring:
  jpa:
    properties:
      hibernate:
        default_batch_fetch_size: 50
// Или через Java Config
@Configuration
public class HibernateConfig {
    @Bean
    public LocalContainerEntityManagerFactoryBean entityManagerFactory() {
        // ...
        properties.put("hibernate.default_batch_fetch_size", 50);
        return factory;
    }
}

Типичные ошибки

// ❌ Слишком маленький размер
@BatchSize(size = 1)  // эквивалентно отсутствию @BatchSize

// ❌ Слишком большой размер (проблемы с БД)
@BatchSize(size = 1000)  // может превысить лимит параметров в IN clause
// Oracle: максимум 1000 параметров
// PostgreSQL: зависит от конфигурации

// ❌ Использование для @ManyToOne без понимания
@ManyToOne
@BatchSize(size = 25)
private User user;
// Работает, но имеет смысл только когда много сущностей ссылаются на разные User

Сравнение с @Fetch

// @BatchSize — загружает пакетами при обращении
@BatchSize(size = 25)
private List<OrderItem> items;

// @Fetch(SUBSELECT) — один подзапрос для всех
@Fetch(FetchMode.SUBSELECT)
private List<OrderItem> items;

// Комбинация — лучший подход
@BatchSize(size = 25)
@Fetch(FetchMode.SUBSELECT)
private List<OrderItem> items;

🔴 Senior Level

Внутренняя реализация

В Hibernate 6+ batch fetching работает эффективнее благодаря улучшенному алгоритму формирования IN-clause.

Механизм работы @BatchSize:

1. При первом обращении к proxy коллекции:
   - Hibernate проверяет BatchSize
   - Собирает доступные ID родительских сущностей

2. Формирует запрос:
   - WHERE parent_id IN (id1, id2, ..., idN)
   - N = min(@BatchSize, доступные ID, default_batch_fetch_size)

3. Загружает все связанные сущности одним запросом
4. Кэширует их в Persistence Context
5. При обращении к другим коллекциям — берёт из кэша

Важно:
- Реальный размер батча = min(@BatchSize, hibernate.default_batch_fetch_size)
- Hibernate динамически подбирает размер IN clause

Ограничения баз данных

Oracle:       максимум 1000 параметров в IN clause
PostgreSQL:   зависит от max_stack_depth и prepared statement
MySQL:        зависит от max_allowed_packet
SQL Server:   до 2100 параметров

Оптимизация в production

// Настройка через Hibernate 6
spring.jpa.properties.hibernate.default_batch_fetch_size=50

// Для конкретных сущностей — переопределение
@Entity
@BatchSize(size = 100)  // больше для часто используемых
public class User { }

// Меньше для больших коллекций
@OneToMany
@BatchSize(size = 10)
private List<LogEntry> logs;

Комбинирование стратегий

@Entity
public class Order {

    // JOIN FETCH для обязательных данных
    @Query("SELECT DISTINCT o FROM Order o JOIN FETCH o.user WHERE o.id = :id")
    Order findByIdWithUser(@Param("id") Long id);

    // @BatchSize для дополнительных данных
    @OneToMany(mappedBy = "order")
    @BatchSize(size = 25)
    private List<OrderItem> items;

    // EntityGraph для динамической загрузки
    @EntityGraph(attributePaths = {"items", "payments"})
    List<Order> findByStatus(String status);
}

Мониторинг и отладка

# Включить логирование SQL для проверки
spring.jpa.show-sql: true
spring.jpa.properties.hibernate.format_sql: true

# Для подсчёта запросов
logging.level.org.hibernate.SQL: DEBUG
logging.level.org.hibernate.type.descriptor.sql.BasicBinder: TRACE

Best Practices

✅ @BatchSize(10-50) для коллекций
✅ Глобальный default_batch_fetch_size = 25-50
✅ Комбинирование с JOIN FETCH
✅ Учёт ограничений БД (IN clause лимиты)
✅ Мониторинг реального количества запросов

❌ Слишком маленький размер (< 5)
❌ Слишком большой размер (> 100 требует проверки: убедитесь что ваша БД поддерживает такой IN clause и что запрос не превышает max_allowed_packet. Для справочников с 5000 записями batch size 100 может быть оптимальным.)
❌ Без мониторинга запросов
❌ Использование вместо JOIN FETCH когда данные нужны всегда

🎯 Шпаргалка для интервью

Обязательно знать:

  • @BatchSize загружает связанные сущности пакетами: N запросов → N/batchSize
  • Реальный размер = min(@BatchSize, hibernate.default_batch_fetch_size)
  • Оптимальный диапазон: 10-50, слишком маленький — не помогает, слишком большой — проблемы с IN clause
  • Можно использовать на коллекциях, классах, и глобально через default_batch_fetch_size
  • БД имеют лимиты: Oracle — 1000 параметров, SQL Server — 2100
  • Комбинируется с JOIN FETCH и SUBSELECT для optimal results

Частые уточняющие вопросы:

  • Где можно использовать @BatchSize? На коллекциях (@OneToMany), на классах, глобально через конфигурацию
  • Что если parents загружались по одному? @BatchSize всё равно работает — собирает ID при обращении
  • @BatchSize vs @Fetch(SUBSELECT)? SUBSELECT — один запрос для всех родителей, BatchSize — разбивает на группы
  • Когда @BatchSize бесполезен? Одна сущность, используется JOIN FETCH, или коллекция очень маленькая

Красные флаги (НЕ говорить):

  • «Ставлю @BatchSize(size = 1000)» — превысит лимит параметров БД
  • «@BatchSize заменяет JOIN FETCH» — это дополняющие, а не заменяющие механизмы
  • «Глобальный размер 1000 для всего» — проблемы с IN clause и производительностью
  • «Не мониторю SQL после @BatchSize» — без мониторинга не видно эффект

Связанные темы:

  • [[1. Что такое проблема N+1 и как её решить]]
  • [[5. Какие стратегии fetch существуют в Hibernate]]
  • [[28. Как использовать JOIN FETCH для решения проблемы N+1]]
  • [[10. Что такое кэш второго уровня и когда его использовать]]