JavaRush /Курсы /Hibernate deep-dive /AUTO flush до конца ...

AUTO flush до конца транзакции

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

1. Почему AUTO flush кажется «внезапным»

Если вы когда-нибудь смотрели SQL‑лог и ловили чувство: «я же просто хотел прочитать данные, почему Hibernate внезапно сделал UPDATE?», поздравляю: вы уже сталкивались с auto flush. Эта лекция как раз о том, что Hibernate не «стреляет SQL сам по себе», а старается сохранить корректность запросов внутри транзакции — иногда ценой того, что SQL уходит раньше, чем вы ожидали.

Представьте типичный кусок кода в нашем проекте Commerce Persistence Lab: мы нашли Product, поменяли имя, а потом решили выполнить какой-нибудь запрос. На уровне Java это выглядит «безобидно», но на уровне ORM это означает: в persistence context есть несинхронизированные изменения, и дальше Hibernate должен решить — можно ли выполнять запрос к БД прямо сейчас или сначала нужно «подтянуть» базу к текущему состоянию unit of work.

Что значит режим AUTO и какую гарантию он пытается дать

По умолчанию большинство приложений живёт в режиме flush примерно по принципу: «как получится, но так, чтобы запросы не врали». В JPA это называется FlushModeType.AUTO, и именно этот режим вы обычно получаете, даже если ни разу в жизни не писали слово flush руками. Но нас здесь интересует более узкий вопрос: в каких ситуациях Hibernate делает flush автоматически ещё до конца транзакции. Сравнивать режимы имеет смысл уже после того, как сама механика AUTO перестанет казаться магией.

Главная идея очень простая, почти скучная: Hibernate старается, чтобы внутри одного @Transactional метода ваш код мог рассуждать так, будто он работает с «консистентной реальностью». Вы поменяли Product.name в managed‑сущности, а потом сделали JPQL‑запрос про товары — Hibernate должен сделать всё возможное, чтобы запрос увидел изменения, которые уже приняты внутри текущего unit of work. Иначе получается абсурд: объект в памяти уже “Phone Pro”, а запрос к базе говорит “Phone”, и оба вроде бы «правда», но вам от этого не легче.

3. Триггер №1: flush перед commit (самый ожидаемый)

Здесь важно сразу сместить опору: flush логически привязан не к концу метода, а к концу транзакции. Часто это совпадает, потому что @Transactional стоит на методе, но магическая граница здесь именно транзакция. И даже если flush произошёл раньше, «настоящий финал» всё равно — commit или rollback.

Рассмотрим самый простой сценарий: мы изменили managed‑сущность и больше не делали запросов. Тогда SQL действительно «дотерпит» до коммита.

import com.example.commerce.catalog.entity.Product;
import jakarta.persistence.EntityManager;
import org.springframework.transaction.annotation.Transactional;

@Transactional
public void renameProduct(EntityManager em) {
    // 1) Загружаем managed-сущность: она попадает в persistence context
    Product p = em.find(Product.class, 1L); // SELECT

    // 2) Меняем поле: SQL ещё не уходит, это только изменение в памяти (dirty state)
    p.setName("Phone Pro");

    // 3) SQL UPDATE обычно уйдёт на flush при завершении транзакции (перед commit)
}

Что здесь важно заметить глазами: p.setName("Phone Pro") не обязан генерировать SQL прямо сейчас. Dirty checking просто отмечает, что объект изменился. А вот когда Spring будет завершать транзакцию, Hibernate выполнит flush, и в этот момент SQL уйдёт в БД.

Ещё один нюанс, который часто ломает мозг начинающим: flush перед commit — это «почти всегда», но всё равно не равен commit. SQL может быть отправлен, а транзакция потом откатится.

4. Триггер №2: flush перед JPQL/HQL

Большинство «внезапных апдейтов» в логах появляется именно перед JPQL/HQL‑запросами. Hibernate делает flush, чтобы запрос к базе не работал по устаревшему состоянию. Если хочется бытовой аналогии, представьте: вы начали редактировать документ, ещё не нажали «Сохранить», а потом вдруг решили выполнить поиск по документам на диске. Чтобы поиск выдал актуальный результат, редактор должен сначала сохранить файл. Hibernate делает примерно то же самое: перед некоторыми запросами он «сохраняет» ваши изменения через flush, чтобы база не отвечала по старому состоянию.

Что такое «перекрывающийся запрос» простыми словами

Перекрывающийся (overlapping) запрос — это запрос, который читает те же данные (точнее: те же таблицы/пространства данных), по которым у вас уже есть несинхронизированные изменения в persistence context. Hibernate понимает: если он сейчас отправит SELECT, база ответит, исходя из старого состояния строк. Но ведь внутри текущей транзакции мы уже считаем, что изменения «произошли» (по крайней мере, в модели). Значит, для корректности нужно сделать flush перед выполнением запроса.

Важно: Hibernate не читает ваши мысли и не анализирует смысл запроса как человек. Он действует прагматично: «если запрос может быть затронут изменениями — лучше перестраховаться». Из‑за этого flush иногда выполняется чуть более консервативно, чем вам хотелось бы, но зато без сюрпризов в корректности.

Пример: изменили Product, затем читаем Product — flush случится до запроса

Чтобы увидеть это вживую, достаточно сочетания «изменение + JPQL‑запрос по той же сущности». Здесь Hibernate почти вынужден выполнить flush до SELECT, чтобы запрос вернул данные с учётом изменений текущего unit of work.

import com.example.commerce.catalog.entity.Product;
import jakarta.persistence.EntityManager;
import org.springframework.transaction.annotation.Transactional;

@Transactional
public void renameAndCountProducts(EntityManager em) {
    // 1) Загружаем Product и держим его managed в persistence context
    Product p = em.find(Product.class, 1L);

    // 2) Меняем имя: пока что это только изменение в памяти
    p.setName("Phone Pro");

    // 3) Дальше идёт JPQL по Product: запрос "перекрывается" с изменениями,
    //    поэтому Hibernate может сделать AUTO flush перед выполнением SELECT
    Long cnt = em.createQuery("select count(p) from Product p", Long.class)
            .getSingleResult();

    // 4) Результат count считаем "консистентным" с текущей транзакцией
    System.out.println("cnt = " + cnt); // cnt = 20 (пример)
}

Если включён SQL‑лог, картина часто выглядит примерно так (упрощённо):

-- SELECT product by id
-- мы изменили name в памяти
update product set name=? where id=?;      -- flush before query
select count(*) from product;              -- сам JPQL запрос

Ключевой момент: UPDATE ушёл не «в конце», а перед SELECT, потому что запрос читает Product, а у нас есть несинхронизированные изменения Product. Hibernate делает flush, чтобы запрос к БД видел «текущую правду транзакции».

Контраст: изменили Product, затем читаем Customer — flush может не сработать

Теперь возьмём похожий код, но запросим сущности, которые не пересекаются с изменёнными данными. В нашем проекте это, например, Customer.

import com.example.commerce.catalog.entity.Product;
import com.example.commerce.customer.entity.Customer;
import jakarta.persistence.EntityManager;
import org.springframework.transaction.annotation.Transactional;

@Transactional
public void renameProductAndListCustomers(EntityManager em) {
    // 1) Изменили Product в persistence context
    Product p = em.find(Product.class, 1L);
    p.setName("Phone Pro");

    // 2) Запросили Customer: по умолчанию это "не перекрывается" с Product,
    //    поэтому flush перед запросом может и не понадобиться
    em.createQuery("select c from Customer c", Customer.class)
            .setMaxResults(5)
            .getResultList();

    // 3) UPDATE по product обычно уйдёт позже, перед commit (или перед другим перекрывающимся запросом)
}

Идея здесь проста: запрос читает таблицу клиентов, а наши изменения касаются товаров. Hibernate может решить, что flush перед таким запросом не нужен, и отложить синхронизацию до завершения транзакции.

Важно отдельно подчеркнуть слово «может». В реальной жизни ваша задача как инженера — не гадать, а включать SQL trace и смотреть, что делает именно ваша конфигурация и именно ваша версия Hibernate. Но как ментальная модель по умолчанию это работает: нет перекрытия — нет причины принудительно вызывать flush.

«Невидимое перекрытие»: JOIN’ы и запросы, где Product участвует, но не в select

Этот подпункт нужен, потому что многие начинающие воспринимают перекрытие слишком буквально: «если в JPQL написано from Customer — значит, flush не будет». Но в JPQL легко сделать запрос, который формально возвращает PurchaseOrder, а фактически читает ещё и Product через join. И тогда перекрытие внезапно появляется.

Представим, что мы изменили Product.status, а потом хотим получить заказы, в которых есть товары нужного статуса. Запрос корневой по PurchaseOrder, но таблица product там участвует.

import com.example.commerce.catalog.entity.Product;
import com.example.commerce.catalog.entity.ProductStatus;
import com.example.commerce.orders.entity.PurchaseOrder;
import jakarta.persistence.EntityManager;
import org.springframework.transaction.annotation.Transactional;

@Transactional
public void updateProductAndQueryOrders(EntityManager em) {
    // 1) Меняем Product: изменения пока только в persistence context
    Product p = em.find(Product.class, 1L);
    p.setStatus(ProductStatus.ACTIVE);

    // 2) Запрос "про заказы", но через join затрагивает Product,
    //    поэтому это реальное перекрытие, и flush перед SELECT вполне вероятен
    em.createQuery("""
            select o from PurchaseOrder o
            join o.items i
            join i.product p
            where p.status = :st
            """, PurchaseOrder.class)
        .setParameter("st", ProductStatus.ACTIVE)
        .setMaxResults(10)
        .getResultList();
}

Hibernate видит: запрос затрагивает данные по Product, а у нас есть несинхронизированные изменения Product. Чтобы запрос был корректным (и, например, увидел товар уже “ACTIVE”), он может выполнить flush до выполнения SELECT. И вот вы снова видите в логах UPDATE product ... перед тем, как прочитать список заказов, хотя «вроде бы» читаете не товары, а заказы.

5. Триггер №3: native SQL‑запрос через EntityManager

Native SQL через EntityManager — особый случай: здесь Hibernate гораздо хуже понимает, что именно вы собираетесь читать. JPQL/HQL он ещё как-то анализирует: какие сущности участвуют, какие таблицы потенциально затронуты. А native SQL для него — это «чёрный ящик»: вы могли написать запрос к product, а могли к purchase_order, а могли к половине схемы с хитрым CTE. Поэтому auto flush перед native SQL‑запросом — вполне логичная перестраховка.

Но держим рамку узкой: ниже речь именно о createNativeQuery(...) через JPA EntityManager. Если вы идёте в native SQL другим путём, правила синхронизации нужно проверять уже в контексте этого API, а не переносить вывод автоматически.

Почему native SQL особенно часто вызывает flush

Hibernate должен выбирать между двумя плохими вариантами. Первый — не делать flush и рискнуть, что native‑запрос увидит старые данные и вернёт результат, противоречащий изменениям в persistence context. Второй — сделать flush «на всякий случай», даже если запрос на самом деле не пересекается с изменениями. В режиме AUTO Hibernate обычно выбирает корректность и предсказуемость результата, а не «экономию одного flush».

И снова: это не про «магию», а про простую инженерию. Запрос к базе должен быть согласован с тем, что ORM уже накопил в unit of work.

Пример: createNativeQuery(...) после изменения entity

Возьмём максимально прямолинейный пример: поменяли Product.name и затем сделали native запрос по таблице product.

import com.example.commerce.catalog.entity.Product;
import jakarta.persistence.EntityManager;
import org.springframework.transaction.annotation.Transactional;

@Transactional
public void renameAndRunNativeSql(EntityManager em) {
    // 1) Меняем managed-сущность: пока что это только изменение в памяти
    Product p = em.find(Product.class, 1L);
    p.setName("Phone Pro");

    // 2) Native SQL для Hibernate — "чёрный ящик", поэтому перед запросом он часто делает flush
    Number cnt = (Number) em.createNativeQuery("select count(*) from product")
            .getSingleResult();

    System.out.println("cnt = " + cnt); // cnt = 20 (пример)
}

Очень типичная картина в SQL‑логе:

update product set name=? where id=?;      -- flush before native query
select count(*) from product;              -- native SQL

И снова: commit ещё не произошёл, но SQL уже ушёл. Это означает, что внутри вашей транзакции база уже «видит» изменения (потому что они отправлены в БД), а другие транзакции — всё ещё нет (потому что commit не случился). Такой «полупрогресс» и есть нормальная жизнь транзакции: можно многое сделать и даже отправить SQL, но пока не закоммитили — это всё ещё можно откатить.

Короткий контекст: почему это особенно важно, если вы смешиваете JPA и JDBC

Этот подпункт нужен не затем, чтобы учить вас жить на JDBC, а чтобы объяснить мотив auto flush. В реальных сервисах иногда встречаются смешанные сценарии: часть логики делает JPA, часть — JdbcTemplate или какой-нибудь репозиторий на чистом SQL. И вот тут случается удивление: «я же поменял entity, почему мой SQL через JDBC не видит изменения?»

Ответ тот же: до flush база не видит ваши изменения, потому что они живут в persistence context. Hibernate умеет подстраховаться перед запросами, которые сам выполняет через EntityManager (JPQL или native SQL). Но если вы напрямую дёрнете JDBC без участия Hibernate, ему сложно угадать, что «пора синхронизироваться». Поэтому помнить про flush в таких смешанных сценариях жизненно полезно. (Мы ещё аккуратно вернёмся к наблюдению и ручной фиксации момента синхронизации в пятой лекции дня.)

Как это выглядит на шкале времени

Здесь полезно на минуту отойти от отдельных примеров и увидеть общую картину: AUTO flush — это не отдельная команда в вашем коде, а встроенный механизм. Вы могли просто дойти до SELECT, а «унитаз» Hibernate уже решил, что пора смывать. Шутка грубоватая, но, к сожалению, очень запоминающаяся: flush и правда может сработать «сам», потому что такова политика AUTO.

Ниже — упрощённая схема последовательности событий внутри одного @Transactional метода.

sequenceDiagram
    participant Code as "Java-код (Service)"
    participant PC as Persistence Context
    participant DB as PostgreSQL

    Code->>DB: SELECT Product by id
    DB-->>Code: "Product (managed)"
    Code->>PC: product.setName("Phone Pro")
    Note over PC: "Изменение только в памяти, dirty checking \"пометил\""

    Code->>PC: JPQL query по Product
    Note over PC: AUTO flush before query
    PC->>DB: UPDATE product ...
    PC->>DB: SELECT ... from product ...
    DB-->>Code: result list

    Code->>PC: "конец @Transactional метода"
    Note over PC: "commit -> (возможный) flush -> commit"

Смысл схемы такой: точка flush может появиться не только «на выходе», но и «в середине», если Hibernate считает, что иначе запрос будет некорректен.

6. Типичные ошибки при AUTO flush

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

Ошибка №2: думать, что любой SELECT обязательно вызывает flush.
Реальность тоньше: flush в AUTO делается, когда запрос перекрывается с накопленными изменениями, или когда Hibernate не может надёжно гарантировать корректность без flush (что часто бывает с native SQL). Поэтому модель «любой SELECT вызывает flush» слишком груба. Но и модель «SELECT никогда не вызывает flush» — ещё хуже.

Ошибка №3: не замечать перекрытие из‑за JOIN’ов.
Очень частая ловушка: вы изменили Product, а запрос делаете по PurchaseOrder, и ожидаете «разные сущности — значит flush не нужен». Но если в запросе есть join i.product p, то таблица product участвует, и перекрытие вполне реальное. Поэтому в голове нужно держать не только названия классов в select, но и то, какие таблицы реально участвуют в запросе.

Ошибка №4: воспринимать native SQL как «просто ещё один SELECT, только в строке».
Для Hibernate native SQL — это почти всегда потеря прозрачности. Он не обязан понимать вашу строку SQL, а значит будет действовать консервативно: лучше сделать flush заранее, чем вернуть некорректный результат. Если вы увидели flush перед native SQL‑запросом — это не баг и не «Hibernate сошёл с ума», а попытка сохранить единое представление данных.

Ошибка №5: путать «SQL ушёл» и «данные точно сохранены навсегда».
Flush отправляет SQL, но транзакция ещё может откатиться. Это особенно важно, когда вы видите в логах UPDATE, а потом в конце метода случается исключение. Новички иногда думают: «ну всё, раз UPDATE ушёл — база испорчена». Нет: если транзакция откатилась, изменения не будут зафиксированы. SQL‑лог показывает движение, но окончательное решение всё ещё за commit/rollback.

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