1. Тайминг: изменение поля и SQL
Если вы только начинаете, очень легко мыслить так: «Я вызвал setName(), значит в базе уже новое имя». Потом вы включаете SQL-логи и видите, что UPDATE появляется не там, где вы ожидали. А иногда — наоборот: UPDATE «вылетает внезапно», ещё до конца метода. Это ощущается как фокус, но на самом деле это вполне логичная инженерная модель.
Hibernate (и вообще ORM) работает по принципу «сначала аккуратно соберём изменения в памяти, а потом синхронизируем их с базой в удобный момент». Внутри транзакции это похоже на черновик: вы редактируете данные, Hibernate помечает «вот это поменяли», а SQL отправляет тогда, когда надо синхронизироваться. Самое важное — запомнить три разных слоя: обнаружение изменений (dirty checking), отправка SQL (flush) и финальная фиксация (commit).
Чтобы не держать это в голове как три разрозненные фразы, давайте сделаем короткую карту процесса.
sequenceDiagram
participant S as "Service method (@Transactional)"
participant PC as Persistence Context
participant DB as PostgreSQL
S->>DB: "SELECT (findById)"
DB-->>S: row -> entity
S->>PC: entity becomes managed
S->>PC: "change field (dirty)"
Note over PC: dirty checking marks entity as changed
S->>PC: flush (auto or explicit)
PC->>DB: UPDATE / INSERT / DELETE SQL is executed
S->>DB: COMMIT
2. flush: синхронизация с базой
Слово flush звучит как «смыть», и это, честно, неплохая ассоциация: Hibernate «смывает» накопленные изменения из persistence context в базу данных — то есть выполняет SQL, который соответствует вашим изменениям. При этом он не закрывает транзакцию и не делает изменения «вечными». Он просто доводит базу до состояния «в базе выполнены нужные INSERT/UPDATE/DELETE в рамках текущей транзакции».
Полезно думать о flush как о моменте, когда Hibernate перестаёт держать изменения только «в голове» и действительно отправляет их в PostgreSQL. Но важно не перепутать: после flush объект остаётся managed, persistence context никуда не исчезает, а транзакцию всё ещё можно откатить. Если вы вызвали flush(), а потом бросили исключение, то с высокой вероятностью будет rollback — и база вернётся в исходное состояние, как будто вы ничего не делали (по крайней мере с точки зрения финального результата).
Вот маленькая таблица — её можно сохранить как «шпаргалку в голове»:
| Событие | Что происходит | Что не происходит |
|---|---|---|
| dirty checking | Hibernate замечает, что managed-entity изменили | SQL ещё может не отправляться |
| flush | Hibernate отправляет SQL в БД (в рамках текущей транзакции) | Транзакция ещё не фиксируется навсегда |
| commit | БД фиксирует изменения и делает их видимыми другим транзакциям | Не обязан отправлять SQL «впервые» (он мог уйти раньше на flush) |
Теперь — к практическому ощущению. Если вы меняете поле у managed-сущности, UPDATE может «ждать» до flush. А flush может случиться автоматически или явно.
3. commit: фиксация транзакции
После слова flush мозг часто хочет успокоиться: «Ну всё, SQL ушёл, значит сохранено». Но транзакционный мир устроен чуть хитрее: SQL может быть выполнен, но пока транзакция не зафиксирована, изменения не считаются окончательно принятыми. commit — это момент, когда база говорит: «Окей, всё, это теперь истина, это durable state».
И вот здесь тонкий, но важный момент: commit — это не операция Hibernate, это операция базы данных (точнее, JDBC-транзакции, которой управляет Spring через transaction manager). Hibernate участвует в этом процессе как «исполнитель», который перед коммитом обязан убедиться, что изменения синхронизированы. Поэтому перед commit почти всегда происходит flush (даже если вы его явно не делали).
Если хочется очень земной аналогии, то flush — это как «я отправил документ на принтер, он уже печатается», а commit — «я поставил подпись и отправил в архив, теперь отменить нельзя». До коммита вы всё ещё можете сказать: «Стоп, я передумал» — и сделать rollback.
4. Авто-flush: коммит и запросы
Сейчас будет момент, где многие впервые перестают злиться на Hibernate и начинают с ним дружить. Hibernate не делает flush «по приколу». Он делает его в ситуациях, когда иначе нарушится логика unit of work. Два самых частых сценария: перед коммитом и перед выполнением запроса, результат которого должен учитывать ваши изменения.
Авто-flush перед commit
В конце транзакции Spring будет делать commit (если всё ок) или rollback (если упало исключение). Перед commit Hibernate обязан синхронизировать изменения с БД, иначе коммитить просто нечего. Поэтому если вы ничего явно не вызывали, то типичный поток такой: вы меняете поля у managed-entity → Hibernate помечает изменения → в конце транзакции делается flush → потом commit.
Пример, который обычно вызывает удивление у новичков: здесь нет save(), но в базе всё меняется.
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class ProductAdminService {
private final ProductRepository productRepository;
public ProductAdminService(ProductRepository productRepository) {
this.productRepository = productRepository;
}
@Transactional
public void renameProduct(Long productId, String newName) {
// Загружаем сущность: после findById она становится managed в persistence context
Product product = productRepository.findById(productId).orElseThrow();
// Меняем поле у managed-сущности: dirty checking запомнит изменение
product.setName(newName);
// UPDATE обычно уйдёт на auto-flush перед commit
}
}
Если вы включили SQL-логи, UPDATE вы увидите ближе к концу транзакции (часто прямо перед commit). И это нормально.
Авто-flush перед SELECT-запросом
Вот тут начинается самое интересное. Допустим, внутри транзакции вы изменили сущность, а потом вызвали репозиторный метод, который делает SELECT и зависит от этих изменений. Если Hibernate не сделает flush перед запросом, вы получите парадокс: в памяти сущность уже «активная», а запрос в БД выполнится по старому состоянию и может вернуть другой результат. Чтобы не ломать «read your writes» в рамках unit of work, Hibernate часто делает flush перед выполнением запросов.
Покажем это на примере: меняем статус товара, а потом читаем список активных товаров.
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class CatalogReadWriteService {
private final ProductRepository productRepository;
public CatalogReadWriteService(ProductRepository productRepository) {
this.productRepository = productRepository;
}
@Transactional
public void activateAndQuery(Long productId) {
// Получаем managed-сущность внутри транзакции
Product product = productRepository.findById(productId).orElseThrow();
// Меняем состояние: Hibernate отметит entity как dirty
product.setStatus(ProductStatus.ACTIVE);
// Дальше идёт запрос, который должен "увидеть" свежие изменения в рамках unit of work
productRepository.findByStatus(ProductStatus.ACTIVE);
// Перед SELECT Hibernate может сделать flush -> UPDATE уйдёт ДО SELECT
}
}
Если вы посмотрите SQL-логи, вас может удивить порядок: сначала UPDATE, потом SELECT. Но это не «странность». Это Hibernate пытается сделать так, чтобы запрос в рамках этой же транзакции увидел ваши изменения.
5. Явный flush: когда нужен
Поскольку flush часто происходит автоматически, логичный вопрос: «А зачем тогда вообще существует явный flush()?». Ответ спокойный: он нужен, когда вы хотите контролировать момент, когда SQL уходит в базу, не завершая транзакцию. Это полезно в некоторых сценариях, но вредно, если начать «флашить после каждого чиха».
Один из самых понятных смыслов явного flush — «fail fast». Допустим, вы сделали несколько шагов бизнес-операции и хотите убедиться, что база уже приняла изменения. Тогда вы вызываете flush(), получаете возможное исключение раньше и не продолжаете делать лишнюю работу. Даже без ухода в детали ограничений сам принцип «поймать ошибку раньше» полезен уже сейчас.
Давайте посмотрим на демонстрацию: мы меняем данные, делаем flush, а потом специально падаем, чтобы увидеть, что flush не равен commit.
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class FlushDemoService {
private final ProductRepository productRepository;
public FlushDemoService(ProductRepository productRepository) {
this.productRepository = productRepository;
}
@Transactional
public void flushButRollback(Long productId) {
// Меняем managed-сущность: изменение пока "в контексте", а не обязательно в БД
Product product = productRepository.findById(productId).orElseThrow();
product.setName("Name from flush demo");
// Явно просим Hibernate отправить SQL в БД (но транзакция ещё не зафиксирована)
productRepository.flush(); // SQL может уйти в БД прямо сейчас
// Специально валимся, чтобы увидеть rollback после уже выполненного SQL
throw new IllegalStateException("Boom! rollback please");
}
}
Если вы запустите это и потом проверите в базе, вы увидите, что итоговое значение не поменялось, потому что транзакция откатилась. То есть SQL мог выполняться, но финального commit не случилось.
Ещё один полезный смысл явного flush — диагностика. Когда вы учитесь, очень удобно «вставить flush» и увидеть, в какой момент Hibernate реально отправляет SQL. Это как поставить System.out.println() в нужном месте, только для базы.
Но вот чего не стоит делать: превращать flush() в «обязательный финальный штамп» после каждого save() или после каждого setXxx(). Обычно это делает код шумным, а транзакцию — менее эффективной (потому что вы заставляете Hibernate слишком часто синхронизироваться с БД).
6. flush в Spring Data и JPA
На практике вы встретите три основных способа инициировать flush. И тут важно не устроить «религиозную войну», а понять, что это просто разные уровни API. В нашем учебном проекте shop-data-jpa чаще всего достаточно репозиторных методов — они понятнее студенту и читаются как «вот здесь мы явно синхронизируем».
Сначала покажем короткую таблицу:
| Инструмент | Где находится | Когда удобно |
|---|---|---|
| productRepository.flush() | Spring Data JpaRepository | Когда вы работаете через репозиторий и хотите явно синхронизировать изменения |
| productRepository.saveAndFlush(entity) | Spring Data JpaRepository | Когда вы сохраняете новый объект и хотите сразу отправить INSERT |
| entityManager.flush() | JPA API (EntityManager) | Когда вы на более низком уровне и хотите показать «вот что под капотом» |
flush() на репозитории
Это самый понятный вариант: «Я работаю с ProductRepository, и я же говорю ему: синхронизируйся». В проекте это выглядит естественно.
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class CatalogFlushService {
private final ProductRepository productRepository;
public CatalogFlushService(ProductRepository productRepository) {
this.productRepository = productRepository;
}
@Transactional
public void updatePriceAndFlush(Long productId) {
// Меняем managed-сущность: Hibernate накопит изменение до flush/commit
Product product = productRepository.findById(productId).orElseThrow();
product.setPrice(product.getPrice().add(new java.math.BigDecimal("10.00")));
// Явно синхронизируемся с БД, не завершая транзакцию
productRepository.flush(); // SQL UPDATE уйдёт раньше конца транзакции
}
}
saveAndFlush(): «сохрани и сразу синхронизируй»
Эта штука полезна, когда вы создаёте новый объект, делаете save(), и вам по какой-то причине важно, чтобы INSERT ушёл прямо сейчас (а не «когда-нибудь на auto-flush»). В обычных бизнес-сценариях это нужно нечасто, но в учебных лабораториях и в диагностике — удобно.
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class CategoryCreateService {
private final CategoryRepository categoryRepository;
public CategoryCreateService(CategoryRepository categoryRepository) {
this.categoryRepository = categoryRepository;
}
@Transactional
public Long createAndFlush() {
Category category = new Category();
category.setCode("TEA");
category.setName("Tea");
// saveAndFlush = сохранить (persist/merge) + сразу отправить SQL (flush), но не commit
Category saved = categoryRepository.saveAndFlush(category);
return saved.getId();
}
}
Обратите внимание: saveAndFlush() не «делает коммит». Он просто делает save, потом flush, а транзакция всё ещё идёт.
EntityManager.flush(): полезно понимать
Поскольку мы всё-таки учим data-layer, полезно один раз увидеть EntityManager и понять, что репозитории — это удобная обёртка. Но превращать проект в «ручное EntityManager-программирование» мы не будем.
import jakarta.persistence.EntityManager;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class EntityManagerFlushService {
private final EntityManager entityManager;
private final ProductRepository productRepository;
public EntityManagerFlushService(EntityManager entityManager,
ProductRepository productRepository) {
this.entityManager = entityManager;
this.productRepository = productRepository;
}
@Transactional
public void flushViaEntityManager(Long productId) {
// Сущность остаётся managed, мы просто управляем моментом отправки SQL
Product product = productRepository.findById(productId).orElseThrow();
product.setName("Flush via EM");
// Явный flush через JPA API (по сути то же самое, что и repository.flush())
entityManager.flush(); // тот же смысл: отправить SQL
}
}
7. flush в логах и commit
Если вы не смотрите на SQL, flush и commit так и останутся мистикой. Но если включить логирование аккуратно, то внезапно становится видно: «ага, вот тут UPDATE, вот тут SELECT, а вот тут метод закончился». Проблема только в том, что можно включить логи так, что консоль превратится в «Матрицу», и вы захотите выключить компьютер и уйти выращивать кактусы.
В рамках учебного режима обычно достаточно двух вещей: показывать SQL и показывать параметры (или хотя бы понимать, что параметры есть). Настройки профилей и логирования мы уже делали раньше, поэтому сейчас я просто напомню принцип: включайте SQL-логи в отдельном dev-профиле, чтобы в обычной работе проект не шумел.
И вот пример того, как может выглядеть фрагмент логов (упрощённо), когда flush происходит перед запросом:
-- вы поменяли status у Product (dirty checking)
update product set status=? where id=?;
/* дальше — запрос, которому нужно увидеть свежие изменения в рамках этой транзакции */
select p.id, p.name from product p where p.status=?;
Логика такая: Hibernate понял, что сейчас будет SELECT, который зависит от текущих изменений, сделал flush, отправил UPDATE, потом сделал SELECT.
Ещё раз: это не означает, что «всё уже сохранено навсегда». Это означает, что SQL выполнен в рамках транзакции, но финальная точка — всё равно commit.
8. Мини-сценарии из mini-shop
В нашем проекте Mini Shop Data Layer почти любой write-use-case — это не один save, а несколько шагов: найти товар, проверить остатки, создать заказ, пересчитать сумму, обновить остатки. Внутри одной транзакции Hibernate держит managed-объекты и изменения в persistence context, а SQL отправляет тогда, когда нужно синхронизироваться.
Представьте упрощённый кусок логики: мы «резервируем остаток», а потом тут же делаем проверочный запрос (например, считаем, сколько товаров стало “out of stock”). Если запрос должен учитывать изменения, Hibernate может сделать flush перед ним — и это правильно, иначе ваш запрос будет жить в прошлой реальности.
Чтобы почувствовать это на коде, можно представить такой сервисный метод (опять же, он демонстрационный):
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class InventoryStatsService {
private final StockItemRepository stockItemRepository;
public InventoryStatsService(StockItemRepository stockItemRepository) {
this.stockItemRepository = stockItemRepository;
}
@Transactional
public long reserveAndCount(Long stockItemId) {
StockItem item = stockItemRepository.findById(stockItemId).orElseThrow();
// Меняем managed-сущность: это может повлиять на результат последующего запроса
item.setReservedQuantity(item.getReservedQuantity() + 1);
// Любой countBy... — это запрос в БД, и перед ним возможен auto-flush
return stockItemRepository.countByReservedQuantityGreaterThan(0);
// перед count-запросом может быть flush
}
}
Даже если countBy... выглядит «невинно», это всё ещё запрос к базе. И если Hibernate понимает, что результат зависит от текущих изменений, он будет стараться синхронизировать изменения перед запросом. Внутри одной транзакции это помогает сохранить предсказуемость.
9. Типичные ошибки при работе с flush и commit
Ошибка №1: считать flush синонимом commit.
Это самая популярная путаница. flush — это отправка SQL и выполнение команд в базе внутри транзакции, а commit — финальная фиксация результата. После flush вы всё ещё можете получить rollback, и тогда в базе «как будто ничего и не было». Если вы на code review видите фразу «ну мы же тут flush сделали, значит уже сохранили», это хороший повод мягко (или не очень) уточнить, что именно человек имеет в виду.
Ошибка №2: ожидать, что SQL всегда уходит строго в конце сервисного метода.
В голове новичка метод @Transactional часто воспринимается как «коробка», где ничего не происходит, а в конце — бац, и всё улетело. На практике Hibernate может сделать auto-flush перед SELECT, перед commit, а иногда и по другим причинам. Это не «хаос», это попытка обеспечить согласованность чтения в рамках unit of work.
Ошибка №3: вызывать flush() после каждой маленькой правки поля.
Так можно случайно превратить ORM в очень нервного курьера, который бегает в базу после каждого setXxx(). Обычно это не даёт пользы, но повышает стоимость транзакции, усложняет отладку и делает код шумным. Явный flush — это инструмент точечного контроля и обучения, а не «обязательный ритуал приличного человека».
Ошибка №4: не понимать, что flush — это не “очистка” контекста.
Иногда flush путают с тем, что «контекст сбрасывается и всё становится detached». Нет: flush не делает entity detached. Она всё ещё managed, всё ещё живёт в persistence context, и dirty checking по-прежнему будет работать дальше в той же транзакции.
Ошибка №5: менять данные в readOnly-транзакции и удивляться, что они не сохранились.
Мы уже обсуждали readOnly как маркер намерения. В некоторых конфигурациях read-only транзакции могут влиять на поведение flush (например, Hibernate старается не делать лишнюю синхронизацию). Поэтому если вы случайно меняете поля entity в @Transactional(readOnly = true), вы можете получить очень странный эффект: в памяти вы «поменяли», а в базе нет. Правильная дисциплина здесь простая: изменения данных живут в write-транзакциях, а чтения — в read-only, и вы не смешиваете эти роли «по привычке».
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ