HibernateSeniorTechnical

Что такое метод flush() в Hibernate и когда сессия сбрасывается автоматически?

flush() синхронизирует изменения persistence context с БД, генерируя SQL INSERT/UPDATE/DELETE, но не делает COMMIT. Автоматический flush срабатывает перед выполнением JPQL/HQL-запроса (FlushMode.AUTO) и перед commit транзакции.

flush() в Hibernate

session.flush() записывает накопленные изменения из первого уровня кэша (persistence context) в БД в виде SQL-операций. Важно: flush — это не commit. Данные попадают в текущую транзакцию, но не становятся видимыми другим транзакциям до вызова tx.commit().

Что происходит при flush

  1. Dirty checking — Hibernate сравнивает текущее состояние каждого managed-объекта со snapshot, сделанным при его загрузке.
  2. Генерация SQL — для изменённых объектов формируются UPDATE, для новых — INSERT, для удалённых — DELETE.
  3. Сортировка операций — по умолчанию Hibernate сортирует SQL согласно ActionQueue: сначала INSERT, затем UPDATE, затем DELETE (с учётом foreign key порядка).
  4. Выполнение через JDBC — SQL отправляется в БД в рамках текущей транзакции.

FlushMode — когда flush происходит автоматически

// FlushMode.AUTO (умолчание в Session)
// flush срабатывает:
// 1. Перед выполнением HQL/JPQL-запроса, если запрос может затронуть изменённые данные
// 2. Перед commit транзакции

Session session = sessionFactory.openSession();
Transaction tx = session.beginTransaction();

User user = session.get(User.class, 1L);
user.setEmail("new@example.com");     // dirty — изменение зафиксировано в контексте

// Следующий запрос затрагивает таблицу users → AUTO flush сработает ДО запроса
List<User> users = session.createQuery("from User where active = true", User.class)
                           .list();   // UPDATE user SET email=? выполнится перед этим SELECT

tx.commit();   // ещё один flush + commit
session.close();

Режимы FlushMode

// FlushMode.MANUAL — flush только явным вызовом session.flush()
session.setHibernateFlushMode(FlushMode.MANUAL);

User user = session.get(User.class, 1L);
user.setName("Test");

// Запрос НЕ вызовет flush автоматически
List<User> all = session.createQuery("from User", User.class).list();
// user.getName() в all может быть старым! Stale read внутри транзакции.

session.flush(); // явный flush

// FlushMode.COMMIT — flush только перед commit
session.setHibernateFlushMode(FlushMode.COMMIT);

// FlushMode.ALWAYS — flush перед каждым запросом (дорого, но предсказуемо)
session.setHibernateFlushMode(FlushMode.ALWAYS);

Явный flush и ConstraintViolationException

Session session = sessionFactory.openSession();
Transaction tx = session.beginTransaction();
try {
    Product product = new Product();
    product.setSku("DUPLICATE-SKU");   // нарушает UNIQUE constraint
    session.persist(product);

    // Явный flush позволяет поймать ConstraintViolationException ДО commit
    // и принять решение (rollback, логирование) в этом же блоке
    session.flush();
    tx.commit();
} catch (ConstraintViolationException e) {
    tx.rollback();
    log.error("SKU уже существует: {}", e.getMessage());
}

flush() в Spring @Transactional

В Spring при использовании EntityManager (JPA) flush вызывается автоматически перед коммитом транзакции. Явный entityManager.flush() нужен для получения сгенерированного ID до закрытия транзакции или для принудительной проверки constraint в batch-операциях.

@Transactional
public Long createUser(CreateUserDto dto) {
    User user = new User(dto.email());
    entityManager.persist(user);
    entityManager.flush();   // Hibernate выполнит INSERT и заполнит user.getId()
    return user.getId();     // ID доступен до commit транзакции
}

Подводные камни

  • FlushMode.AUTO анализирует, пересекается ли запрос с «dirty» таблицами, но эвристика неточна — иногда flush не срабатывает перед нативным SQL-запросом (createNativeQuery), что приводит к stale read.
  • При FlushMode.MANUAL JPQL-запросы будут возвращать данные, не учитывающие изменения в текущей сессии — это тонкий источник багов в batch-операциях.
  • Batch insert/update: каждый session.flush() + session.clear() должен вызываться с интервалом, равным hibernate.jdbc.batch_size, иначе первый уровень кэша вырастет до OOM.
  • ConstraintViolationException при flush делает транзакцию unusable — даже если поймать исключение, нужен rollback; попытка продолжить работу с той же Session приведёт к непредсказуемому поведению.
  • Порядок SQL в ActionQueue может не совпадать с порядком вызовов в коде — если foreign key constraint требует строгого порядка INSERT, используйте session.flush() между операциями явно.
  • В Hibernate 6 изменилась логика dirty checking: используется bytecode enhancement вместо reflection по умолчанию — это быстрее, но требует корректной конфигурации плагина при сборке.
  • Открытый в Spring EntityManager в рамках Open-Session-In-View автоматически вызывает flush в конце HTTP-запроса — изменения, сделанные в сервисном слое, могут неожиданно закоммититься даже без явного @Transactional на контроллере.
  • Вызов flush() на read-only сессии (session.setDefaultReadOnly(true)) не генерирует SQL — это не ошибка, но может вводить в заблуждение при отладке.

Common mistakes

  • Путать термин «flush» с соседним механизмом Hibernate.
  • Не называть границу lifecycle, transaction, thread или request для «flush».
  • Игнорировать production-эффекты «flush»: latency, SQL shape, memory, security или observability.

What the interviewer is testing

  • Попросить объяснить механизм «flush» на минимальном примере.
  • Проверить, видит ли кандидат failure mode и диагностику для «flush».
  • Уточнить, какие настройки или API меняют «flush» в реальном сервисе.

Sources

Related topics