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

Що робить анотація @BatchSize

@BatchSize — одна з найкорисніших анотацій Hibernate для боротьби з проблемою N+1. Вона дозволяє завантажувати пов'язані сутності пакетами (batch), значно скорочуючи кількість S...

Мовні версії: 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. Що таке кеш другого рівня і коли його використовувати]]