Що робить анотація @BatchSize
@BatchSize — одна з найкорисніших анотацій Hibernate для боротьби з проблемою N+1. Вона дозволяє завантажувати пов'язані сутності пакетами (batch), значно скорочуючи кількість S...
Огляд
@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 марний
- Завантажується одна сутність — batch не потрібен
- Використовується JOIN FETCH — дані вже завантажені
- Колекція маленька (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. Що таке кеш другого рівня і коли його використовувати]]