JavaRush /Курсы /Hibernate deep-dive /flush: фаза синхрони...

flush: фаза синхронизации

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

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 должен быть осознанной точкой синхронизации, а не ритуалом.

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