1. Симптом: SQL раньше конца транзакции
Если вы уже смотрели SQL-лог (а на этом курсе это почти как смотреть погоду перед выходом на улицу), то наверняка ловили странное ощущение: вы ещё внутри сервисного метода, код ещё не закончился, а в логах уже проскочил UPDATE или INSERT. И в голове тут же рождается бытовая теория: «Наверное, Hibernate сам всё закоммитил». Спойлер: нет.
Мы часто сваливаем в одну кучу три разных события: изменение объекта в памяти, отправку SQL в базу и завершение транзакции. В “ручном SQL” они обычно идут почти подряд (вызвали UPDATE — он улетел — закоммитили), поэтому мозг привыкает к связке “SQL = финал”. В Hibernate эти события могут быть разведены по времени. И слово, которое связывает dirty checking со SQL, — flush.
Мы уже видели, что Hibernate умеет заметить изменение managed-сущности ещё до всякого SQL. Теперь нужен следующий кусок картины: в какой момент это замеченное изменение вообще превращается в запрос к базе и почему это отдельная фаза, а не второе название commit.
Три мира: объект в памяти, persistence context и БД
Чтобы flush перестал казаться магией, полезно навести порядок в «географии» приложения. Представьте, что у нас есть три разные «площадки», и одно и то же поле product.name может одновременно существовать в трёх состояниях. Звучит тревожно, но для ORM это нормальная жизнь: он как раз и нужен, чтобы этим управлять.
Первый мир — это обычный Java-объект в памяти. Вы вызываете product.setName("Phone Pro") и меняете поле в объекте. Это происходит мгновенно: без сети, без JDBC и без базы. Второй мир — это persistence context (контекст, где Hibernate держит managed-сущности, snapshots и очередь изменений). Третий мир — это реальная база данных (PostgreSQL в нашем Commerce Persistence Lab), где значение лежит в строке таблицы.
Удобно держать это так (простая схема):
flowchart LR
A["Java-объект (Product) поля меняются через setters"] --> B["Persistence Context managed entity + snapshots + очередь действий"]
B --> C["База данных (PostgreSQL) строки таблиц"]
Ключевой момент такой: когда вы меняете managed-объект, изменения сразу происходят в мире A (объект), и Hibernate начинает понимать, что snapshot отличается (мир B), но это ещё не обязано немедленно менять мир C (БД). Переход B → C как раз и называется flush.
3. Что такое flush — без мистики
В бытовом английском слово flush и правда напоминает смыв в туалете, поэтому иногда хочется думать: «нажал flush — и всё улетело навсегда». В Hibernate смысл другой и куда более инженерный. flush — это синхронизация состояния persistence context с базой данных: Hibernate берёт накопленные изменения managed-сущностей, превращает их в SQL-команды (INSERT/UPDATE/DELETE) и отправляет их в БД в рамках текущей транзакции.
Важно: flush не «ищет изменения из воздуха». Он опирается на dirty checking: Hibernate фиксирует, что именно изменилось, и готовится к тому, что при синхронизации это придётся записать в базу. Последовательность здесь такая: мы изменили объект → Hibernate это заметил (dirty checking) → при flush это превращается в реальный SQL.
Можно представить это как очень короткий внутренний алгоритм (не точный байткод Hibernate, но правильная модель):
// Модель: что примерно происходит внутри Hibernate при синхронизации
1) В persistence context есть managed-объекты.
2) Для каждого managed-объекта есть snapshot.
3) Перед flush Hibernate сравнивает snapshot и текущее состояние.
4) Если есть отличия — формирует SQL.
5) Отправляет SQL в БД (но транзакцию не закрывает).
Отсюда уже видно, почему flush — это не commit: flush отправляет SQL, а commit завершает транзакцию. Отправить SQL можно несколько раз за одну транзакцию, а «закрыть сделку» — только один раз.
4. flush и commit: разные уровни финальности
Путаница между flush и commit дорого обходится прежде всего потому, что заставляет неправильно читать SQL-лог. Когда вы видите UPDATE в логе, интуитивно хочется решить: «всё, запись точно сохранена». Но в транзакционном мире это «точно» наступает на commit, а не на flush.
Давайте аккуратно сравним:
| Характеристика | flush | commit |
|---|---|---|
| Что делает | Синхронизирует изменения из persistence context в БД, отправляя SQL | Завершает транзакцию в БД (фиксирует изменения) |
| Может происходить несколько раз в одной транзакции | Да | Нет (логически один раз на транзакцию) |
| Гарантирует, что изменения “останутся навсегда” | Нет (возможен rollback) | Да (если commit успешен) |
| Может привести к ошибке constraint/уникальности раньше конца метода | Да | Да, но часто вы увидите проблему позже, если не было flush |
| Связан с dirty checking | Напрямую: flush использует результаты dirty checking | Косвенно: commit обычно вызывает flush (или приводит к нему) |
Если хочется более «человечной» аналогии, используйте такую: flush — это как нажать Ctrl+S (сохранить черновик на диск), а commit — как нажать Publish (опубликовать статью) или «завершить оплату». Вы могли сохранить файл, но потом отменить изменения, откатить ветку или вообще не отправить их пользователю. В транзакции роль «отменить» играет rollback.
И ещё важный нюанс: flush происходит внутри транзакции. Это означает, что SQL выполняется в БД, но другие транзакции обычно не обязаны видеть эти изменения до commit (мы не уходим в изоляции и уровни видимости, просто фиксируем базовую идею).
5. Примеры flush: три мини-сцены
Пример 1: setter не равен SQL
Ключевая мысль здесь такая: setter — это не SQL. Даже если вы изменили managed entity, Hibernate не обязан в ту же миллисекунду писать UPDATE. Он может подождать до момента синхронизации. И это не «ленивость», а нормальная архитектура unit of work.
Представим, что мы работаем в Commerce Persistence Lab и меняем имя товара. Пример нарочно маленький — чтобы увидеть суть, а не утонуть в слоях.
import com.example.commerce.catalog.entity.Product;
import jakarta.persistence.EntityManager;
// Получаем managed-сущность: она попадает в persistence context (мир B)
Product product = entityManager.find(Product.class, 1L);
// Меняем поле только в памяти (мир A)
product.setName("Phone Pro");
// В этот момент Product уже изменён в памяти,
// Hibernate заметил изменение (dirty checking),
// но UPDATE ещё не обязан быть отправлен в БД (мир C).
По смыслу происходит следующее: find() возвращает managed-объект. Вы меняете поле, dirty checking запоминает, что объект «грязный» — то есть отличается от snapshot. Но отправлять SQL пока рано: Hibernate ещё не дошёл до точки синхронизации.
И вот здесь нужна дисциплина: если смотреть только на Java-код, легко начать думать «почему UPDATE в логе иногда появляется, а иногда нет?». Ответ всегда один: смотрите на момент flush, а не на момент вызова setter.
Пример 2: явный flush() как точка синхронизации
Теперь добавим ровно одну строку — entityManager.flush(). Это как поставить в коде флажок: «вот здесь я хочу, чтобы всё накопленное превратилось в реальный SQL и ушло в базу».
import com.example.commerce.catalog.entity.Product;
import jakarta.persistence.EntityManager;
Product product = entityManager.find(Product.class, 1L);
product.setName("Phone Pro");
// Явно просим Hibernate синхронизировать накопленные изменения с БД.
// Важно: это НЕ commit, транзакция остаётся открытой.
entityManager.flush(); // здесь Hibernate обязан отправить SQL
Если у вас включён SQL trace (а он у нас включаемый профиль), то в логах вы увидите что-то очень похожее на:
-- Пример SQL, который может появиться сразу после flush()
update product set name=? where id=?
Ещё раз: flush() не означает commit. Он означает: «отправь SQL прямо сейчас, но транзакцию не закрывай». Как инструмент наблюдения и диагностики это очень удобно: вы можете специально поставить точку синхронизации и увидеть поведение системы ещё до завершения метода.
Пример 3: flush отправил SQL, но потом случился rollback
Самый коварный для новичка сценарий выглядит так: вы вызываете flush(), видите SQL в логе, радуетесь, а потом в конце метода прилетает исключение, транзакция откатывается — и в базе внезапно «ничего не изменилось». Кажется, что база «игнорирует SQL». На самом деле база честно выполнила SQL внутри транзакции, а потом честно откатила саму транзакцию.
Рассмотрим упрощённый сервисный сценарий (стиль labsupport, чтобы не смешивать это с «боевой» бизнес-логикой). Мы специально бросаем исключение после flush.
import com.example.commerce.catalog.entity.Product;
import jakarta.persistence.EntityManager;
import org.springframework.transaction.annotation.Transactional;
@Transactional
public void renameProductAndFail(EntityManager entityManager) {
// Старт транзакции на уровне Spring (границы важны для понимания rollback)
Product product = entityManager.find(Product.class, 1L);
// Меняем managed-сущность: пока это изменение живёт в persistence context
product.setName("Draft name");
// Принудительная синхронизация: SQL уйдёт в БД в рамках ОТКРЫТОЙ транзакции
entityManager.flush(); // SQL ушёл
// Исключение приводит к rollback: изменения не станут "навсегда"
throw new RuntimeException("rollback"); // но commit не произойдёт
}
Что вы должны вынести из этого примера как «мантру»:
flush может привести к реальному SQL, но до commit это всё ещё обратимо.
Да, есть тонкие нюансы вроде последовательностей (sequence) в PostgreSQL, которые могут увеличить значение даже при rollback, но это отдельная история про генерацию идентификаторов. В контексте сегодняшней лекции достаточно запомнить базовую вещь: данные, изменённые через INSERT/UPDATE/DELETE внутри транзакции, не считаются «навсегда», пока транзакция не закоммичена.
6. flush в unit of work: короткая временная шкала
Когда вы читаете SQL-лог в этом курсе, очень помогает держать в голове не «строчки Java», а «фазы unit of work». В Hibernate почти всё интересное происходит именно на границах фаз. flush — одна из таких границ: она отделяет «изменения накоплены в памяти» от «изменения отправлены в БД».
Удобная временная шкала выглядит так:
sequenceDiagram
participant Code as Java-код (Service)
participant PC as Persistence Context
participant DB as PostgreSQL
Code->>PC: "find() -> managed entity"
Code->>PC: "setName(\"Phone Pro\")"
Note over PC: dirty checking видит изменение
Code->>PC: "flush() (явно или автоматически)"
PC->>DB: "отправка SQL (UPDATE/INSERT/DELETE)"
Note over DB: SQL выполнен, но транзакция ещё открыта
Code->>DB: "commit (в конце транзакции)"
Эта картинка нужна не ради красоты, а для ежедневной практики: вы будете видеть SQL «раньше конца метода», и ваша задача — не паниковать, а спросить себя: «где тут flush?». Если flush был — всё логично. Если вы его явно не вызывали, значит, сработал автоматический триггер синхронизации, и именно он обычно создаёт ощущение «SQL выстрелил сам».
7. Типичные ошибки при первом знакомстве с flush
Прежде чем идти дальше, полезно зафиксировать типичные ошибки. Они коварны тем, что внешне всё выглядит «работающим», а модель в голове собирается неправильная. А неверная модель в persistence layer почти всегда заканчивается тем, что вы лечите симптомы, а не причину.
Ошибка №1: считать, что flush и commit — одно и то же.
Если вы так думаете, то любой SQL в середине метода будет казаться «ранним коммитом», и вы начнёте подозревать Spring, Hibernate, фазу Луны и соседа по креслу. Правильная модель проще: flush отправляет SQL в рамках открытой транзакции, а commit — закрывает транзакцию и делает изменения окончательными.
Ошибка №2: ждать SQL в момент вызова setter.
Новички иногда читают код как «вызвал setName() — значит, сейчас будет UPDATE». Но setter меняет Java-объект. Hibernate может подождать до точки синхронизации. В итоге человек смотрит на лог, не видит UPDATE, добавляет save(), потом добавляет ещё один save(), а потом удивляется, почему запросов стало больше, а ясности — меньше.
Ошибка №3: игнорировать dirty checking и думать, что flush «сам всё решит».
flush — не магическая команда «сохрани всё подряд». Он синхронизирует то, что Hibernate считает изменённым. Если изменений нет — flush() может вообще не дать никакого UPDATE. И наоборот: если вы случайно поменяли поле (например, мутировали value object), то flush честно отправит SQL. Без модели dirty checking это будет восприниматься как хаос.
Ошибка №4: делать выводы по Java-коду, не глядя на SQL-лог.
Hibernate многое делает во время выполнения, и значительная часть его поведения проявляется только в SQL. По коду вы видите «я поменял имя», а по SQL — «в какой момент это стало UPDATE». Если копать глубже, это разные вопросы. И если вы хотите перестать «верить ORM на слово», придётся привыкнуть: SQL-лог — ваш детектор правды.
Ошибка №5: радоваться flush() как «кнопке надёжности» и ставить её везде.
После первого «озарения» иногда появляется новая религия: «давайте после каждого изменения делать flush(), тогда точно всё будет ок». Это примерно как лечить любую проблему System.out.println() — иногда помогает, но чаще просто делает всё медленнее и шумнее. flush должен быть осознанной точкой синхронизации, а не ритуалом.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ