Как работает механизм flush в Hibernate
Сброс контекста (flush) — это внутренний процесс Hibernate и JPA, во время которого все накопленные в оперативной памяти изменения сущностей (INSERT, UPDATE, DELETE) транслируют...
Сброс контекста (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().