JavaRush /Курсы /Hibernate deep-dive /Режимы flush в Hibe...

Режимы flush в Hibernate

Hibernate deep-dive
4 уровень , 2 лекция
Открыта

1. Flush mode: автосохранение для БД

Если flush — это сам факт синхронизации, то flush mode — это политика, которая отвечает на вопрос: «А когда ты, Hibernate, вообще имеешь право или обязан сделать flush автоматически?» Это похоже на настройки автосохранения в редакторе текста: можно сохранять каждые N минут, можно только при закрытии, можно выключить автосохранение и жать Ctrl+S руками, а можно поставить режим «сохраняйся после каждого чиха» — и быстро возненавидеть жизнь.

Важно уловить нюанс: flush mode не отменяет dirty checking и не меняет состояние сущности. Ваш Product как был managed и «грязный», так им и останется. Меняется только то, когда Hibernate решит «перевести грязь в SQL» автоматически. Именно отсюда растут многие «внезапные UPDATE», «почему запрос не видит мои изменения» и «почему я ничего не сохранял, а оно полезло в базу».

Мы уже видели это на AUTO: момент flush зависит не только от конца транзакции, но и от того, какую корректность Hibernate пытается удержать для запросов. Теперь вопрос шире: что меняется, если переключить саму политику автоматической синхронизации.

Для ориентира можно держать в голове простую цепочку:

flowchart TD
    A[Вы поменяли managed-entity] --> B[dirty checking отмечает изменения]
    B --> C{"flush mode решает когда flush допустим/обязателен"}
    C --> D[flush отправляет SQL]
    D --> E[commit/rollback завершает транзакцию]

2. Режимы JPA и Hibernate

Путаница с flush mode начинается там же, где начинается реальная жизнь: у нас есть стандарт JPA, и у нас есть Hibernate как конкретная реализация, которая умеет больше. Из‑за этого существует два «списка режимов», и они похожи названиями, но относятся к разным API.

В мире JPA вы работаете с jakarta.persistence.FlushModeType, и там всего два режима: AUTO и COMMIT. Это как меню в столовой: «суп» и «суп, но в другой тарелке». В мире Hibernate есть org.hibernate.FlushMode, где режимов больше: AUTO, COMMIT, MANUAL, ALWAYS. Это уже похоже на кофемашину в офисе: от «эспрессо» до «сделай мне латте с пенкой, но чтобы жизнь стала лучше».

В нашем проекте Commerce Persistence Lab мы обычно стартуем с JPA-уровня (через EntityManager), а если надо — аккуратно «проваливаемся» в Hibernate Session.

Небольшая шпаргалка в табличном виде:

Уровень API Enum Доступные режимы Что это означает в двух словах
JPA FlushModeType AUTO, COMMIT Стандартное поведение и «отложи до коммита»
Hibernate FlushMode AUTO, COMMIT, MANUAL, ALWAYS То же + «только руками» + «перед каждым запросом»

И да, это нормально, что в одном приложении вы встречаете оба: Spring Data JPA разговаривает с вами на языке JPA, а когда нужно копнуть глубже в Hibernate, приходится пользоваться его нативными возможностями.

3. AUTO: режим по умолчанию

AUTO — это базовая политика, от которой удобно отталкиваться. Hibernate делает flush перед commit и может сделать его раньше, если иначе запрос внутри транзакции увидит устаревшее состояние относительно уже накопленных изменений. Мы это уже разобрали на overlapping query, count(...) и native query через EntityManager: смысл AUTO не в том, чтобы «писать чаще», а в том, чтобы не врать запросам внутри текущего unit of work.

Поэтому здесь AUTO важен прежде всего как точка отсчёта. COMMIT ослабляет эту автоматическую синхронизацию, MANUAL почти выключает её, а ALWAYS делает максимально агрессивной. Дальше нас интересует именно этот контраст.

4. COMMIT: flush в конце транзакции

Режим COMMIT звучит как хорошая оптимизация: «давай без ранних UPDATE, сделай всё в конце». И по сути так и есть: Hibernate старается не делать автоматический flush перед запросами и откладывает синхронизацию до момента commit. Но расплата очевидна: запросы внутри транзакции могут работать так, будто ваших изменений «ещё нет», потому что физически в базе их и правда пока нет.

Это не баг и не «сломанный Hibernate». Это прямой контракт режима: вы сознательно говорите ORM-движку «не пытайся обеспечить корректность промежуточных запросов за счёт раннего flush». Поэтому COMMIT нужно применять только тогда, когда вы точно понимаете, что делаете, и точно не строите логику на «свежести» базы посреди транзакции.

Пример, в котором разница становится очевидной, — тот же count(...). В COMMIT flush перед запросом может не происходить, и count будет считаться по состоянию БД «до ваших изменений».

import jakarta.persistence.EntityManager;
import jakarta.persistence.FlushModeType;

// Важно: это меняет flush mode для текущего persistence context (EntityManager)
entityManager.setFlushMode(FlushModeType.COMMIT);

Product p = entityManager.find(Product.class, 1L); // managed-сущность
p.setName("Phone Pro (commit)"); // делаем изменения, но в БД они пока не обязаны попасть

Long count = entityManager.createQuery(
        "select count(p) from Product p where p.name = :name", Long.class)
    .setParameter("name", "Phone Pro (commit)") // параметр для запроса
    .getSingleResult(); // в COMMIT flush перед SELECT обычно не происходит

System.out.println("count = " + count); // часто будет 0, потому что UPDATE ещё не ушёл

Обратите внимание на психологическую ловушку: вы изменили объект, а запрос говорит «нет, таких в базе нет». И оба правы. Объект изменён в persistence context, но база ещё не синхронизирована, потому что вы попросили режим COMMIT.

Когда COMMIT бывает уместен? Он бывает уместен в сценариях, где вы делаете набор изменений и почти не выполняете «перекрывающих» запросов до конца транзакции. Например, вы загружаете заказ, меняете статус, меняете пару полей и просто выходите из метода — без сложной аналитики посередине. Но если вы начинаете что-то «допроверять» запросами, особенно агрегатами, легко получите странную картину.

5. MANUAL: flush только руками

Режим MANUAL — это когда вы выключаете автосохранение и говорите Hibernate: «Дружище, ты ничего сам не отправляешь. Хочешь SQL — я позову». Это Hibernate-специфичный режим, на JPA-уровне его нет. Обычно он встречается либо в очень осознанной оптимизации, либо в read-only потоках, либо… в коде, который кто-то написал однажды в пятницу вечером и теперь его боятся трогать.

Если говорить совсем прямо, это самый честный режим: без явного flush() SQL не уйдёт. Даже если сущность dirty, даже если вы сделали setName(). Состояние будет жить только в persistence context. Это значит, что запросы к базе будут видеть старые данные, пока вы явно не синхронизируете контекст.

Чтобы включить MANUAL, нам нужно провалиться в Hibernate Session:

import jakarta.persistence.EntityManager;
import org.hibernate.FlushMode;
import org.hibernate.Session;

// unwrap даёт доступ к Hibernate-специфичным настройкам (включая MANUAL/ALWAYS)
Session session = entityManager.unwrap(Session.class);
session.setHibernateFlushMode(FlushMode.MANUAL); // отключаем авто-flush

Product p = entityManager.find(Product.class, 1L); // managed-сущность
p.setName("Phone Pro (manual)"); // изменения есть в persistence context, но не в БД

// В MANUAL не рассчитываем ни на flush перед запросами, ни на flush как побочный эффект commit.
// Если нужна синхронизация — вызываем flush() явно.
session.flush(); // принудительно отправляем SQL прямо сейчас

Практическое правило тут жёсткое: включили MANUAL — не рассчитываем ни на flush перед запросами, ни на flush как побочный эффект commit. Если нужна синхронизация, вызываем flush() явно.

Почему этот режим опасен для новичка? Потому что он даёт максимально «тихий» провал: вы поменяли сущность, транзакция завершилась, а изменения могут не оказаться в БД (или окажутся не тогда, когда вы думали). Поэтому MANUAL нельзя включать «чтобы было меньше запросов» без чёткой стратегии: где именно вы делаете flush, когда проверяете ограничения, как гарантируете корректность.

Да, иногда MANUAL используют как страховку: «не дай бог что-то случайно обновится в read-only сценарии». Но даже тогда это должно быть архитектурно согласовано, а не прилеплено как пластырь.

6. ALWAYS: flush перед каждым запросом

ALWAYS — это противоположность COMMIT. Если COMMIT говорит «не трогай базу до конца», то ALWAYS говорит «перед любым запросом — убедись, что база синхронизирована с контекстом». Звучит безопасно? На уровне корректности — да. На уровне производительности и неожиданных побочных эффектов — иногда это катастрофа.

В ALWAYS вы можете получить flush даже перед запросом, который никак не связан с изменённой сущностью. То есть вы поменяли Product, а потом просто пошли читать Customer — и всё равно получите UPDATE product ... перед SELECT customer ... Для человека, который читает лог, это выглядит как «почему customer-запрос вызывает update товара?», а ответ прост: потому что вы включили режим, который делает flush всегда.

import jakarta.persistence.EntityManager;
import org.hibernate.FlushMode;
import org.hibernate.Session;

// Переходим на Hibernate Session, чтобы включить ALWAYS
Session session = entityManager.unwrap(Session.class);
session.setHibernateFlushMode(FlushMode.ALWAYS); // flush перед КАЖДЫМ запросом

Product p = entityManager.find(Product.class, 1L); // managed-сущность
p.setName("Phone Pro (always)"); // делаем изменения в контексте

entityManager.createQuery("select c from Customer c", Customer.class)
    .getResultList(); // в ALWAYS flush произойдёт перед этим запросом, даже если он про Customer

На практике ALWAYS редко бывает хорошим выбором для кода бизнес-логики. Он скорее встречается в специфических технических сценариях, иногда — при диагностике, когда вы хотите принудительно «вытолкать» изменения перед любым чтением и посмотреть, что реально летит в БД. Но держать его включённым как режим по умолчанию — это примерно как поставить будильник каждые 3 минуты «на всякий случай».

7. Настройка flush mode в коде

Самая частая инженерная ошибка с flush mode — не «перепутать AUTO и COMMIT», а выставить режим слишком широко. Flush mode — штука контекстная: она должна быть привязана к конкретному unit of work, к конкретному сценарию чтения/записи. Поэтому полезно понимать, где этот режим можно задавать и какой у него «радиус поражения».

На уровне JPA у вас есть EntityManager#setFlushMode(FlushModeType). Это влияет на поведение автоматического flush внутри текущего persistence context (обычно — текущей транзакции). На уровне Hibernate Session у вас есть setHibernateFlushMode(FlushMode), что даёт доступ к MANUAL и ALWAYS. И, наконец, есть «самый аккуратный» способ: выставлять flush mode на конкретном запросе, чтобы не менять политику для всего контекста.

Вот пример настройки на уровне конкретного запроса: мы не хотим, чтобы этот запрос триггерил flush (и берём на себя риск корректности).

import jakarta.persistence.FlushModeType;
import java.util.List;

// Flush mode задаётся только для ЭТОГО запроса, не меняя политику для всего EntityManager
List<Product> products = entityManager.createQuery(
        "select p from Product p", Product.class)
    .setFlushMode(FlushModeType.COMMIT) // не провоцируем flush перед выполнением запроса
    .getResultList();

Почему это часто лучше, чем entityManager.setFlushMode(COMMIT) на весь метод? Потому что вы локализуете эффект. Если через десять строк в том же методе появится другой запрос, который должен быть корректным относительно изменений, вы не попадёте в ситуацию «я забыл, что перевёл весь EntityManager в COMMIT».

И ещё один практический совет из разряда «экономит часы жизни»: если вам всё-таки нужно временно менять flush mode на EntityManager/Session-уровне, сохраняйте старое значение и возвращайте его обратно. Это выглядит скучно, но скучность — признак надёжности.

8. Пример: одно изменение и count

Чтобы flush modes не оставались «таблицей из документации», удобно держать в голове один повторяемый сценарий из нашего домена. Возьмём простой вариант: в транзакции мы грузим Product, меняем имя, затем выполняем запрос count(...) по новому имени. Это не потому, что так делают в проде, а потому, что count очень наглядно показывает, был ли flush перед запросом.

Сценарий в коде (как идея, не как «копируй-вставляй навсегда») выглядит так:

import jakarta.persistence.EntityManager;
import jakarta.persistence.FlushModeType;

// Шаг 1: получаем сущность в managed-состоянии
Product p = entityManager.find(Product.class, 1L);
p.setName("Flush Demo Name"); // Шаг 2: меняем поле -> сущность dirty

Long count = entityManager.createQuery(
        "select count(p) from Product p where p.name = :name", Long.class)
    .setParameter("name", "Flush Demo Name") // параметр запроса
    .setFlushMode(FlushModeType.AUTO) // попробуйте AUTO/COMMIT и сравните поведение
    .getSingleResult(); // здесь режим может повлиять: будет flush до SELECT или нет

System.out.println("count = " + count); // зависит от режима и flush

Теперь мысленно подставляем режимы:

В AUTO Hibernate, скорее всего, сделает flush перед count, потому что иначе запрос увидит старую базу и вернёт «неправильное число» относительно ваших изменений. В COMMIT он может не делать flush, и count будет считаться по старой базе. В MANUAL flush не произойдёт вообще без явного flush(), то есть count тоже будет по старой базе. В ALWAYS flush случится почти гарантированно перед запросом, даже если вы забудете, что изменяли что-то «в другом месте».

Для закрепления — маленькая таблица ожиданий (опять же, на уровне идеи, а не обещания конкретных цифр для любого набора данных):

Режим Flush перед запросом count Что увидит count Основное ощущение
AUTO Обычно да “Как будто изменения уже в базе” Корректно, но иногда неожиданно рано
COMMIT Обычно нет “Как будто изменения ещё нет” Тише, но можно получить странные промежуточные результаты
MANUAL Нет (только руками) “Как будто изменения нет, пока не flush() Максимальный контроль, максимальная ответственность
ALWAYS Да “Всегда свежее” Дорого и может раздражать количеством flush

Если вы привыкли мыслить через аналогии, то можно запомнить так: AUTO — умное автосохранение, COMMIT — сохранение при закрытии, MANUALCtrl+S, ALWAYS — автосохранение после каждого нажатия клавиши.

9. Типичные ошибки при работе с flush modes

Ошибка №1: воспринимать COMMIT как «безопасное ускорение».
Часто хочется «убрать лишние flush», и рука тянется поставить COMMIT. Но если внутри транзакции есть запросы, особенно агрегатные, вы можете получить результаты, которые противоречат тому, что только что сделали в коде. Это не «Hibernate врёт», это вы поменяли правила игры.

Ошибка №2: включить MANUAL и забыть, что flush теперь не происходит сам.
MANUAL легко превращает изменения в «черновик», который так и останется черновиком. В результате вы видите корректное состояние объектов в памяти, а база — живёт своей жизнью. Особенно коварно это в сервисах, где изменения «как будто прошли», но после перезапуска всё откатилось, потому что SQL так и не был отправлен.

Ошибка №3: ожидать, что flush mode меняет dirty checking или состояние сущности.
Flush mode — это не «режим сохранения сущности». Он не делает entity detached, не превращает managed в transient и не отменяет snapshots. Если сущность managed и вы поменяли поле, dirty checking всё равно это заметит. Вопрос лишь в том, когда это превратится в SQL.

Ошибка №4: включить ALWAYS и потом удивляться, почему «даже чтение» провоцирует записи.
ALWAYS хорош тем, что гарантирует синхронизацию перед любым запросом. Плох он тем, что делает это даже тогда, когда вам совсем не нужно. В логах это выглядит как хаос: вы читаете Customer, а обновляется Product. На самом деле это не хаос, а очень строгий режим, который вы включили.

Ошибка №5: менять flush mode слишком широко и слишком надолго.
Когда flush mode выставляют на уровне EntityManager/Session и забывают вернуть обратно, поведение приложения становится «зависящим от истории выполнения кода». Сегодня этот метод вызвали первым — всё ок, завтра вторым — и SQL пошёл иначе. Поэтому, если режим нужно менять, лучше делать это локально (на запросе) или хотя бы возвращать старое значение обратно.

1
Задача
Hibernate deep-dive, 4 уровень, 2 лекция
Недоступна
Запрос в режиме COMMIT и старое состояние базы
Запрос в режиме COMMIT и старое состояние базы
1
Задача
Hibernate deep-dive, 4 уровень, 2 лекция
Недоступна
Режим MANUAL и два запроса до и после flush
Режим MANUAL и два запроса до и после flush
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ