Що робить анотація @BatchSize
Анотація @BatchSize в Hibernate — це інструмент оптимізації лінивого завантаження (LAZY), який вказує Hibernate завантажувати пов'язані сутності або колекції пакетами (батчами)...
🟢 Junior Level
Що таке @BatchSize простими словами
Анотація @BatchSize в Hibernate — це інструмент оптимізації лінивого завантаження (LAZY), який вказує Hibernate завантажувати пов’язані сутності або колекції пакетами (батчами) через конструкцію WHERE ... IN (?, ?, ...), замість того щоб виконувати окремий SQL-запит на кожен окремий об’єкт.
Це один із найефективніших і найбезпечніших засобів боротьби з проблемою N+1:
\[\text{Кількість SQL-запитів} = 1 + \left\lceil \frac{N}{\text{batchSize}} \right\rceil\]Без @BatchSize (Проблема N+1):
1. SELECT * FROM orders; -- 1 запит, повернув 100 замовлень
2. SELECT * FROM order_items WHERE order_id = 1; -- Запит для замовлення 1
3. SELECT * FROM order_items WHERE order_id = 2; -- Запит для замовлення 2
...
101. SELECT * FROM order_items WHERE order_id = 100; -- 100 запитів! Разом: 101 запит.
З анотацією @BatchSize(size = 25):
1. SELECT * FROM orders; -- 1 запит (100 замовлень)
2. SELECT * FROM order_items WHERE order_id IN (?, ..., ?); -- 1 запит на 25 замовлень одразу!
3. SELECT * FROM order_items WHERE order_id IN (?, ..., ?); -- ще 25 замовлень
4. SELECT * FROM order_items WHERE order_id IN (?, ..., ?); -- ще 25 замовлень
5. SELECT * FROM order_items WHERE order_id IN (?, ..., ?); -- ще 25 замовлень
РАЗОМ: 5 запитів замість 101 (скорочення кількості звернень до бази на 95%)!
Де розміщується анотація
Анотацію можна розмістити у двох місцях із різним ефектом:
1. Над колекцією (@OneToMany / @ManyToMany)
Завантажує пакет дочірніх колекцій для кількох батьківських сутностей одночасно:
@Entity
public class Order {
@Id
private Long id;
@OneToMany(mappedBy = "order", fetch = FetchType.LAZY)
@BatchSize(size = 30) // Підвантажувати позиції пачками по 30 замовлень
private List<OrderItem> items = new ArrayList<>();
}
2. Над класом сутності (@Entity)
Пакетно ініціалізує неініціалізовані проксі батьківських сутностей (@ManyToOne / @OneToOne):
@Entity
@BatchSize(size = 30) // Ініціалізувати проксі User пачками по 30 штук
public class User {
@Id
private Long id;
private String name;
}
🟡 Middle Level
Як Hibernate знаходить сутності для об’єднання в батч
При першому зверненні до лінивого поля одного об’єкта (наприклад, orders.get(0).getItems().size()):
- Hibernate перехоплює звернення через обгортку
PersistentCollection. - Hibernate заглядає в поточний контекст збереження (
PersistenceContext/ L1-кеш) сесії. - Він знаходить усі інші наявні в пам’яті екземпляри сутностей того ж типу (
Order), у яких колекціяitemsще не ініціалізована. - Hibernate бере ідентифікатори знайдених сутностей (до ліміту
size = N) і конструює єдиний SQL-запит з операторомIN. - Отриманий результат розкладається по колекціях відповідних об’єктів
Orderв оперативній пам’яті. - Коли цикл доходить до
orders.get(1).getItems(), дані вже знаходяться в пам’яті — другий SQL-запит взагалі не відправляється!
Глобальне налаштування: default_batch_fetch_size
Замість того щоб вручну розставляти @BatchSize над десятками сутностей і колекцій, у Spring Boot прийнято вмикати глобальне налаштування в application.yml:
spring:
jpa:
properties:
hibernate:
# Рекомендований діапазон для продакшену: від 20 до 50
default_batch_fetch_size: 30
Цей рядок конфігурації автоматично вмикає пакетну вибірку для абсолютно всіх лінивих зв’язків і колекцій у всьому застосунку. Якщо над конкретним полем стоїть @BatchSize, її локальне значення перевизначить глобальне.
🔴 Senior Level
Головна плутанина: Batch Fetching проти JDBC Batch Writing
На співбесідах кандидати часто плутають два абсолютно різні налаштування Hibernate зі словом «batch»:
| Параметр | За що відповідає | Напрямок даних | Що генерує |
|---|---|---|---|
@BatchSize / default_batch_fetch_size |
Читання (Fetching) | З БД у застосунок | SELECT ... WHERE id IN (?, ?, ...) |
hibernate.jdbc.batch_size |
Запис (Flushing) | Із застосунку в БД | JDBC PreparedStatement.addBatch() (INSERT, UPDATE, DELETE) |
Ці налаштування ніяк не пов’язані між собою і вирішують діаметрально протилежні завдання.
Стилі формування запитів: hibernate.batch_fetch_style
Hibernate вміє формувати оператор IN за допомогою різних стратегій (BatchFetchStyle):
DYNAMIC: Hibernate формує точну кількість параметрів уIN (?, ...)під число залишку сутностей.- Мінус: Якщо в пам’яті залишилося 17 об’єктів, згенерується
INіз 17 параметрів, якщо 23 — із 23. База даних змушена щоразу заново парсити SQL і компілювати план виконання (Execution Plan), засмічуючи кеш планів запитів (Prepared Statement Cache).
- Мінус: Якщо в пам’яті залишилося 17 об’єктів, згенерується
PADDED(Стандарт продуктивності для Highload): Розміри батчів округлюються вгору до найближчого степеня двійки ($1, 2, 4, 8, 16, 32, \dots$):-- Якщо потрібно завантажити 23 об'єкти при батчі 32, сформується запит на 32 параметри: SELECT * FROM order_items WHERE order_id IN (?, ?, ... 32 параметри); -- Слоти, яких бракує, дублюють перший ID- Плюс: Кількість унікальних SQL-запитів суворо фіксована (лише кілька варіантів). СУБД кешує скомпільований план запиту, різко знижуючи навантаження на процесор бази даних.
LEGACY: Ділить батч на фіксовані дільники (наприклад, для батчу 32 генеруються форми на 32, 16, 10, 9, … 1).
Увімкнення PADDED-стилю у Spring Boot:
spring:
jpa:
properties:
hibernate:
default_batch_fetch_size: 32
batch_fetch_style: PADDED
Обмеження діалектів СУБД за кількістю параметрів
При виборі розміру батчу сеньйор зобов’язаний враховувати жорсткі ліміти реляційних баз даних:
- Oracle DB: Жорстка межа у 1000 елементів усередині конструкції
IN (...)(помилкаORA-01795: maximum number of expressions in a list is 1000). - Microsoft SQL Server: Ліміт на 2100 параметрів в одному запиті.
- PostgreSQL: Ліміт на $65535$ параметрів (обмеження протоколу
bind).
Рекомендація: Встановлювати
@BatchSizeбільше100практично ніколи не має сенсу. Оптимальне емпіричне значення:30–50. Воно дає 97–98% скорочення мережевих запитів без ризику вичерпання пам’яті та перевищення лімітів СУБД.
4 Tricky Questions
1. Чи працюватиме @BatchSize, якщо батьківські сутності завантажувалися не одним списком, а вибиралися окремо в різних місцях методу?
Відповідь:
Так, працюватиме бездоганно.
У цьому полягає фундаментальна перевага @BatchSize над FetchMode.SUBSELECT:
FetchMode.SUBSELECTвимагає, щоб батьки були витягнуті суворо одним вихідним запитом (він перевикористовує вихідний текст SQL).@BatchSizeабсолютно байдужий до того, як саме сутності потрапили в пам’ять. Він інспектує поточний станPersistenceContext.- Якщо ви виконали
find(Order.class, 1L), потімfind(Order.class, 2L), а потім додали сутності в сесію черезmerge(), при першому зверненні доorder1.getItems()Hibernate знайде в сесії неініціалізованийorder2і вичитає позиції для обох замовлень за один спільний SQL-запитWHERE order_id IN (1, 2).
2. Чи вирішує @BatchSize проблему пагінації з дочірніми колекціями, де пасує JOIN FETCH?
Відповідь:
Так, @BatchSize — це еталонне рішення проблеми пагінації колекцій у JPA.
- При використанні
JOIN FETCHна колекції зPageableHibernate змушений викачувати всю таблицю в пам’ять JVM (HHH000104), оскількиLIMIT/OFFSETна рівні бази спотворюють кількість рядків через декартовий добуток. - При використанні
@BatchSizeзапит за батьками виконується стандартно:SELECT * FROM orders ORDER BY created_at DESC LIMIT 20 OFFSET 0;База даних чесно й ефективно застосовує
LIMITтаOFFSETза індексами. - Потім, коли бізнес-код звертається до
order.getItems(), Hibernate виконує рівно один додатковий запит на 20 замовлень:SELECT * FROM order_items WHERE order_id IN (id1, id2, ... id20); - Підсумок: рівно 2 швидких SQL-запити, пагінація працює на стороні СУБД, пам’ять JVM не перевантажується.
3. Чому @BatchSize на класі @Entity не спрацьовує при пошуку через entityManager.find(User.class, id)?
Відповідь:
Метод find(EntityClass, id) приймає рівно один конкретний первинний ключ.
- У момент виклику
find()у Hibernate немає інших кандидатів для об’єднання в батч. Він виконує звичайний одиночний запитSELECT * FROM users WHERE id = ?. - Анотація
@BatchSizeнад класом спрацьовує тільки при ініціалізації неініціалізованих проксі, коли в поточній сесії вже накопичилося кілька посилань на різні сутності цього типу (наприклад, при завантаженні 50 замовлень, у кожного з яких є поле@ManyToOne User user, представлене проксі).
4. Чи може @BatchSize призвести до деградації продуктивності порівняно зі звичайним LAZY?
Відповідь: Так, у двох специфічних сценаріях:
- Випадковий одиничний доступ: Якщо в сесію завантажено 100 замовлень, але бізнес-логіці потрібно прочитати позиції тільки одного конкретного замовлення, Hibernate з
@BatchSize(size = 50)завантажить позиції відразу для перших 50 замовлень. Виконання запиту займе більше часу, а вичитані 49 колекцій залишаться незатребуваними і даремно займатимуть пам’ять купи (JVM Heap). - Занадто великий розмір батчу (
size = 1000): Запит із гігантським списком параметрівINдовго парситься оптимізатором СУБД, а результуючий пакет мережевих даних може перевищити розмір буфера сокета, збільшуючи мережеву затримку.
🎯 Шпаргалка для інтерв’ю
30-секундна відповідь (Elevator Pitch)
«Анотація
@BatchSizeоптимізує ліниве завантаження (LAZY) в Hibernate, об’єднуючи запити до пов’язаних даних у пакети за допомогою оператораWHERE ... IN (?, ?, ...). Замість $N$ запитів при класичній проблемі N+1 виконується лише $1 + \lceil N / \text{batchSize} \rceil$ запитів. Анотація застосовується до колекцій (@OneToMany) або над класами сутностей (@ManyToOneпроксі). У production-застосунках стандартом є ввімкнення глобального налаштуванняhibernate.default_batch_fetch_size = 30-50, що автоматично захищає весь застосунок від лавини N+1 запитів і є найкращим рішенням для пагінації пов’язаних колекцій».
Ключові метрики для Production-моніторингу
- Кількість SQL-запитів типу
SELECT ... WHERE ... INу метриках Hibernate Statistics. - Відсоток влучань у кеш планів СУБД (Prepared Statement Cache Hit Ratio при використанні
PADDED). - Час відповіді ендпоінтів зі складними ієрархічними зв’язками.
Червоні прапорці (Чого говорити НЕ МОЖНА)
- ❌ «@BatchSize керує пакетною вставкою INSERT у базу даних» — Це фундаментальна помилка:
@BatchSizeкерує виключно читанням (SELECT), а за вставку відповідаєhibernate.jdbc.batch_size. - ❌ «Я ставлю batchSize = 10000, щоб завантажити все одним запитом» — Це призведе до помилок перевищення лімітів СУБД (наприклад, ORA-01795 в Oracle) та перевантаження пам’яті.
- ❌ «@BatchSize замінює JOIN FETCH у всіх випадках» — Для одиничної вибірки об’єкта за ID зі зв’язками
JOIN FETCHефективніший, оскільки робить рівно 1 запит замість 2. - ❌ «@BatchSize не працює зі Spring Data JPA» — Він повністю підтримується, особливо через глобальну властивість
default_batch_fetch_size.