Як працює механізм flush в Hibernate
Скидання контексту (flush) — це внутрішній процес Hibernate та JPA, під час якого всі накопичені в оперативній пам'яті зміни сутностей (INSERT, UPDATE, DELETE) транслюються в SQ...
Скидання контексту (flush) — це внутрішній процес Hibernate та JPA, під час якого всі накопичені в оперативній пам’яті зміни сутностей (INSERT, UPDATE, DELETE) транслюються в SQL-команди і відправляються через драйвер JDBC у мережевий буфер бази даних. Розуміння механіки flush необхідне для запобігання непередбачуваним дедлокам, порушенням обмежень цілісності (Constraints) та управління порядком виконання операцій.
🟢 Junior Level
Що таке flush і чим він відрізняється від commit
Розробники часто плутають поняття flush та commit:
flush(): Переводить стан пам’яті JVM у базу даних. Усі підготовлені оператори SQL (INSERT,UPDATE,DELETE) фізично відправляються до СУБД у межах поточної відкритої транзакції. Зміни стають видимими всередині цієї ж транзакції, але ще не зафіксовані для інших користувачів.commit(): Фіксує транзакцію в СУБД (SQLCOMMIT). У цей момент СУБД записує зміни в журнал транзакцій (WAL), знімає блокування з рядків і робить дані видимими для інших транзакцій. Перед кожним комітом Hibernate автоматично викликаєflush().
FLUSH VS COMMIT У ЖИТТЄВОМУ ЦИКЛІ
[ Java Heap: EntityManager ]
user.setName("Анна");
│
│ 1. entityManager.flush()
▼
[ Сокет JDBC -> СУБД: Поточна транзакція (Не зафіксована) ]
SQL: UPDATE users SET name = 'Анна' WHERE id = 1;
(Дані видно лише поточному з'єднанню, накладено рядкові блокування)
│
│ 2. transaction.commit()
▼
[ СУБД: Постійне сховище (WAL / Data Files) ]
SQL: COMMIT;
(Блокування знято, дані видно всім транзакціям)
Коли Hibernate виконує flush автоматично
- Перед комітом транзакції (
Transaction Commit): Завершення методу@Transactionalзавжди тригерить скидання всіх накопичених змін. - Перед виконанням JPQL / Criteria / Native запитів: Щоб запит повернув актуальний стан даних, змінених у пам’яті поточної сесії (при режимі
FlushModeType.AUTO). - При ручному виклику
entityManager.flush(): Застосовується розробником для примусової синхронізації (наприклад, при пакетній вставці або для отримання згенерованого СУБД ID).
@Transactional
public void exampleFlush() {
User user = entityManager.find(User.class, 1L);
user.setName("Нове ім'я");
// У цей момент у БД ще старе ім'я.
// Явний flush відправляє SQL UPDATE просто зараз:
entityManager.flush();
// Тепер UPDATE відправлено в БД, але транзакція ще НЕ завалідована/закомічена!
// Якщо станеться виняток, транзакція відкотиться (ROLLBACK),
// і зміна "Нове ім'я" зникне з БД.
}
🟡 Middle Level
Режими скидання (FlushModeType)
У JPA та Hibernate поведінка скидання регулюється властивістю FlushModeType:
// Встановлення глобально для сесії
entityManager.setFlushMode(FlushModeType.AUTO); // За замовчуванням
// Або точково для одного конкретного запиту
entityManager.createQuery("SELECT u FROM User u")
.setFlushMode(FlushModeType.COMMIT)
.getResultList();
| Режим | Поведінка | Коли використовувати |
|---|---|---|
FlushModeType.AUTO |
(За замовчуванням) Скидає зміни перед комітом транзакції, а також перед виконанням запитів, які можуть зачепити змінені таблиці. | Стандартний режим розробки. Забезпечує сувору консистентність читання власних змін (Read-Your-Own-Writes). |
FlushModeType.COMMIT |
Скидає зміни тільки під час коміту транзакції. Перед запитами SELECT скидання не виконується. |
Високонавантажені сценарії оптимізації, коли ми точно знаємо, що виконуваний SELECT не залежить від попередніх UPDATE/INSERT. |
Оптимізація AUTO Flush: Простори запитів (Query Spaces)
Ранні версії ORM скидали абсолютно всі сутності перед будь-яким запитом SELECT. Сучасний Hibernate використовує розумну перевірку Query Spaces:
@Transactional
public void smartFlushDemo() {
User user = entityManager.find(User.class, 1L);
user.setName("Олексій"); // Змінено сутність User (таблиця 'users')
// Запит зачіпає зовсім іншу таблицю 'orders':
List<Order> orders = entityManager.createQuery("FROM Order", Order.class).getResultList();
// Hibernate аналізує метамодель: таблиці 'orders' та 'users' не перетинаються!
// FLUSH НЕ БУДЕ ВИКЛИКАНИЙ перед цим запитом! Економія ресурсів CPU та мережі.
}
Виняток: Для нативних SQL-запитів (createNativeQuery) Hibernate не може автоматично проаналізувати зачеплені таблиці, тому за замовчуванням перед нативним SQL він виконує повний flush усіх накопичених змін.
🔴 Senior Level
Черга дій: ActionQueue та суворий порядок виконання SQL
Всередині сесії Hibernate (SessionImpl) усі операції накопичуються в спеціальній структурі даних — черзі дій org.hibernate.engine.spi.ActionQueue.
Коли настає фаза flush(), Hibernate виконує операції НЕ в тому порядку, в якому ви викликали методи в Java-коді, а суворо за внутрішнім регламентом ActionQueue:
СУВОРИЙ ПОРЯДОК ВИКОНАННЯ В ACTIONQUEUE
1. OrphanRemovalAction (Видалення сутностей-сиріт з колекцій)
│
▼
2. EntityInsertAction (Усі операції persist / save)
│
▼
3. EntityUpdateAction (Усі UPDATE, згенеровані Dirty Checking)
│
▼
4. CollectionRemoveAction (Очищення зв'язуючих таблиць @ManyToMany / @OneToMany)
│
▼
5. CollectionUpdateAction (Оновлення елементів колекцій)
│
▼
6. CollectionRecreateAction (Перестворення колекцій)
│
▼
7. EntityDeleteAction (Усі операції remove / delete)
Навіщо потрібен такий порядок:
Такий регламент захищає від помилок зовнішніх ключів (Foreign Key Constraints). Щоб зв’язати батька та дитину, батько має бути вставлений раніше (INSERT parent -> INSERT child). При видаленні — навпаки, спочатку зачищаються зв’язуючі таблиці та колекції, потім дочірні рядки, і лише в кінці — батько.
Пастка унікального обмеження (Unique Constraint Collision)
Через те, що EntityInsertAction (Крок 2) виконується раніше, ніж EntityDeleteAction (Крок 7), виникає класична production-пастка:
// Поле 'username' має обмеження UNIQUE в базі даних
@Transactional
public void replaceUser() {
User oldUser = userRepository.findByUsername("john_doe");
entityManager.remove(oldUser); // Заплановано DELETE
User newUser = new User();
newUser.setUsername("john_doe"); // Той самий унікальний логін
entityManager.persist(newUser); // Заплановано INSERT
// Наприкінці транзакції спрацьовує FLUSH:
// 1. ActionQueue намагається виконати: INSERT INTO users (username) VALUES ('john_doe')
// 2. БД вибухає помилкою: ERROR: duplicate key value violates unique constraint!
// 3. До виконання DELETE справа навіть не доходить!
}
Рішення: Примусово розділити операції через проміжний виклик flush():
entityManager.remove(oldUser);
entityManager.flush(); // Фізично відправляє DELETE у БД просто зараз
entityManager.persist(newUser); // Тепер INSERT пройде успішно!
Оптимізація пакетної обробки (order_inserts та order_updates)
За замовчуванням в ActionQueue вставки та оновлення виконуються в порядку їх додавання. Якщо чергувати різні сутності: User -> Order -> User -> Order, JDBC-драйвер не зможе використати батчинг, оскільки кожен перехід вимагає закриття поточного PreparedStatement.
Увімкнення налаштувань сортування групує однотипні SQL-оператори:
spring:
jpa:
properties:
hibernate:
jdbc.batch_size: 50
order_inserts: true # Сортує INSERT за типами сутностей
order_updates: true # Сортує UPDATE за типами сутностей
4 Tricky Questions
1. Чи можна перехопити виняток порушення зовнішнього ключа або унікальності після entityManager.flush() і продовжити нормальну роботу з тією ж сесією?
Відповідь:
- Категорично ні. Специфікація JPA суворо визначає: при виникненні будь-якого
PersistenceExceptionпід часflush()поточнийPersistenceContextпереходить у нескоординований, неконсистентний стан. - Транзакція негайно позначається драйвером та менеджером транзакцій як
Rollback-Only. - Будь-які спроби виконати подальші запити через цей же
EntityManagerзавершаться помилкою:Transaction rolled back because transaction was marked as rollback-only. - Сесія підлягає негайному закриттю. Якщо потрібно залогувати помилку в БД, це необхідно робити в окремій незалежній транзакції (
@Transactional(propagation = Propagation.REQUIRES_NEW)).
2. Чому виклик entityManager.flush() не звільняє пам’ять JVM, на відміну від entityManager.clear()?
Відповідь:
flush()взаємодіє виключно з базою даних: він генерує SQL і скидає мережевий буфер JDBC.- Усі об’єкти в оперативній пам’яті JVM залишаються в стані
Managedвсередині структурentitiesByKeyтаentityEntriesконтексту персистентності. Посилання на них, як і раніше, сильні (Strong References). - Щоб звільнити пам’ять, необхідно після
entityManager.flush()явно викликатиentityManager.clear(), що розірве зв’язок із сесією і переведе об’єкти в статусDetached.
3. Якщо всередині методу з @Transactional(readOnly = true) викликати entityManager.flush(), що станеться?
Відповідь:
- У Spring Framework анотація
@Transactional(readOnly = true)переводить сесію Hibernate в режимFlushModeType.MANUAL(або вимикає dirty checking). - При виході з методу автоматичне скидання перед комітом взагалі не виконується, заощаджуючи ресурси на перевірку знімків.
- Однак, якщо розробник усередині такого методу явно викличе
entityManager.flush(), Hibernate виконає перевірку dirty checking і спробує відправити SQL у базу даних. - Якщо з’єднання з СУБД на рівні JDBC драйвера було переведено в
Connection.setReadOnly(true), СУБД викине помилку:ERROR: cannot execute UPDATE in a read-only transaction.
4. Чому flush() перед запитом JPQL спрацьовує при пошуку за полями сутності, але НЕ спрацьовує при пошуку за первинним ключем через entityManager.find()?
Відповідь:
- Метод
find(Entity.class, id)у першу чергу опитує кеш першого рівня L1 (entitiesByKey). - Якщо сутність уже знаходиться в пам’яті поточної сесії (навіть якщо її поля були змінені),
find()миттєво повертає вже модифікований об’єкт із пам’яті без звернення до СУБД. Жодної розсинхронізації немає, тому виконувати дорогийflush()не потрібно. - Навпаки, JPQL-запит завжди йде в базу даних через SQL
SELECT. Щоб СУБД побачила змінені рядки у своїх таблицях, Hibernate зобов’язаний попередньо скинути їх черезflush().
🎯 Шпаргалка для інтерв’ю
30-секундна відповідь (Elevator Pitch)
«Механізм
flushу Hibernate синхронізує станPersistenceContextіз базою даних, перетворюючи зміни в пам’яті на SQLINSERT,UPDATEіDELETEта відправляючи їх у буфер транзакції. На відміну відcommit(),flush()не фіксує транзакцію — зміни можна відкотити. За замовчуванням використовуєтьсяFlushModeType.AUTO, коли скидання відбувається перед комітом та перед JPQL-запитами до модифікованих таблиць. Критично важливо пам’ятати проActionQueue: операції виконуються у фіксованому порядку (спочатку всі INSERT, потім UPDATE, і лише наприкінці DELETE). При заміні сутності з UNIQUE-колонкою це вимагає проміжного ручногоflush(), інакше виникне помилка дубліката ключа».
Ключові метрики для Production-моніторингу
- Flush Count (
session.getStatistics().getFlushCount()): аномально високе число свідчить про часті проміжні скидання через неоптимальні запити. - Тривалість Flush Time: відображає час Dirty Checking + мережеві затримки JDBC при передачі пакета SQL.
- Rollback-Only Exceptions Rate: індикатор збоїв під час автоматичного flush перед комітом.
Червоні прапорці (Чого говорити НЕ МОЖНА)
- ❌ «Flush фіксує транзакцію, після нього не можна зробити Rollback» — Груба помилка. Flush лише відправляє SQL, фіксує транзакцію виключно
commit(). - ❌ «Hibernate виконує SQL-запити рівно в тому порядку, в якому я викликаю persist() і remove() у коді» — Порядок диктується
ActionQueue(INSERT завжди передує DELETE). - ❌ «Після падіння flush() з помилкою бази можна зловити виняток у catch і продовжити роботу» — Сесія пошкоджена і знаходиться в статусі
Rollback-Only. - ❌ «Виклик flush() очищає пам’ять JVM від об’єктів» — Пам’ять звільняє лише
entityManager.clear().