💾 Раздел 16 · Вопрос #13

Как работает механизм 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(): Фиксирует транзакцию в СУБД (SQL COMMIT). В этот момент СУБД записывает изменения в журнал транзакций (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 автоматически

  1. Перед коммитом транзакции (Transaction Commit): Завершение метода @Transactional всегда триггерит сброс всех накопленных изменений.
  2. Перед выполнением JPQL / Criteria / Native запросов: Чтобы запрос вернул актуальное состояние данных, измененных в памяти текущей сессии (при режиме FlushModeType.AUTO).
  3. При ручном вызове 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() и продолжить нормальную работу с той же сессией?

Ответ:

  1. Категорически нет. Спецификация JPA жестко определяет: при возникновении любого PersistenceException во время flush() текущий PersistenceContext переходит в нескоординированное, неконсистентное состояние.
  2. Транзакция немедленно помечается драйвером и менеджером транзакций как Rollback-Only.
  3. Любые попытки выполнить последующие запросы через этот же EntityManager завершатся ошибкой: Transaction rolled back because transaction was marked as rollback-only.
  4. Сессия подлежит немедленному закрытию. Если нужно залогировать ошибку в БД, это необходимо делать в отдельной независимой транзакции (@Transactional(propagation = Propagation.REQUIRES_NEW)).

2. Почему вызов entityManager.flush() не освобождает память JVM, в отличие от entityManager.clear()?

Ответ:

  1. flush() взаимодействует исключительно с базой данных: он генерирует SQL и сбрасывает сетевой буфер JDBC.
  2. Все объекты в оперативной памяти JVM остаются в состоянии Managed внутри структур entitiesByKey и entityEntries контекста постоянства. Ссылки на них по-прежнему сильные (Strong References).
  3. Чтобы освободить память, необходимо после entityManager.flush() явно вызвать entityManager.clear(), что разорвет связь с сессией и переведет объекты в статус Detached.

3. Если внутри метода с @Transactional(readOnly = true) вызвать entityManager.flush(), что произойдет?

Ответ:

  1. В Spring Framework аннотация @Transactional(readOnly = true) переводит сессию Hibernate в режим FlushModeType.MANUAL (или выключает dirty checking).
  2. При выходе из метода автоматический сброс перед коммитом вообще не выполняется, экономя ресурсы на проверку снимков.
  3. Однако, если разработчик внутри такого метода явно вызовет entityManager.flush(), Hibernate выполнит проверку dirty checking и попытается отправить SQL в базу данных.
  4. Если соединение с СУБД на уровне JDBC драйвера было переведено в Connection.setReadOnly(true), СУБД выбросит ошибку: ERROR: cannot execute UPDATE in a read-only transaction.

4. Почему flush() перед запросом JPQL срабатывает при поиске по полям сущности, но НЕ срабатывает при поиске по первичному ключу через entityManager.find()?

Ответ:

  1. Метод find(Entity.class, id) в первую очередь опрашивает кэш первого уровня L1 (entitiesByKey).
  2. Если сущность уже находится в памяти текущей сессии (даже если ее поля были изменены), find() мгновенно возвращает уже модифицированный объект из памяти без обращения к СУБД. Никакой рассинхронизации нет, поэтому выполнять дорогостоящий flush() не требуется.
  3. Напротив, JPQL-запрос всегда идет в базу данных через SQL SELECT. Чтобы СУБД увидела измененные строки в своих таблицах, Hibernate обязан предварительно сбросить их через flush().

🎯 Шпаргалка для интервью

30-секундный ответ (Elevator Pitch)

«Механизм flush в Hibernate синхронизирует состояние PersistenceContext с базой данных, преобразуя изменения в памяти в SQL INSERT, 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().

Связанные темы