Когда вы работаете с Hibernate, между вашим Java-кодом и базой данных стоит невидимый посредник — persistence context. Он решает, когда и что отправить в базу, и следит за тем, чтобы один и тот же ряд таблицы не превратился в два разных объекта в памяти.
Что такое persistence context
Persistence context — это рабочая область, которую Hibernate создаёт на время единицы работы (как правило, транзакции). Все сущности, с которыми вы работаете внутри этой области, находятся под наблюдением Hibernate.
В JPA-терминах persistence context создаёт EntityManager. В Hibernate его аналог — Session. На практике Spring управляет этим за вас: один EntityManager живёт ровно в рамках одной транзакции.
@Service
@Transactional
public class OrderService {
private final EntityManager em;
public OrderService(EntityManager em) {
this.em = em;
}
public void confirm(Long orderId) {
Order order = em.find(Order.class, orderId); // сущность теперь managed
order.setStatus(OrderStatus.CONFIRMED); // Hibernate видит изменение
// em.persist() не нужен — flush сам запишет UPDATE
}
}
Identity map: один объект на одну строку
Первое, что делает persistence context — хранит identity map (карту идентичности). Это таблица «первичный ключ → объект», которая гарантирует: если вы дважды загружаете один и тот же ряд в рамках одного persistence context, вы получаете один и тот же Java-объект.
Order a = em.find(Order.class, 1L);
Order b = em.find(Order.class, 1L);
System.out.println(a == b); // true — один объект, второй SELECT не выполнялся
Второй find не идёт в базу вообще — Hibernate смотрит в identity map и отдаёт уже готовый объект. Это первый уровень кэша Hibernate (L1-кэш), встроенный и всегда включённый. Подробнее о кэшировании — в статье /hibernate/caching/.
Четыре состояния сущности
У каждой сущности в любой момент времени есть одно из четырёх состояний. Переходы между ними — через явные вызовы EntityManager или через завершение транзакции.
Transient (переходное)
Объект создан через new, но Hibernate о нём ничего не знает. Нет первичного ключа, нет связи с базой.
Order order = new Order(); // transient
order.setCustomerId(42L);
Managed (управляемое)
Сущность находится в persistence context — Hibernate следит за ней. Любое изменение поля будет автоматически синхронизировано с базой при flush.
Управляемой сущность становится после:
em.persist(entity)— для новых объектов,em.find(...)или JPQL-запроса — для загруженных из базы,em.merge(detachedEntity)— для возвращения detached-объекта.
Detached (отсоединённое)
Сущность была managed, но persistence context закрылся (транзакция завершилась) или вы явно вызвали em.detach(entity). Объект всё ещё существует в памяти и у него есть первичный ключ, но Hibernate его больше не отслеживает.
Order loaded;
// первая транзакция
try (var tx = ...) {
loaded = em.find(Order.class, 1L); // managed
} // транзакция закрыта → loaded стал detached
loaded.setStatus(OrderStatus.CANCELLED); // изменение НЕ уйдёт в базу автоматически
Чтобы вернуть detached-сущность под контроль Hibernate, используйте em.merge(loaded) в новой транзакции. merge создаёт новый managed-объект с данными переданного — не модифицирует переданный напрямую.
Removed (удалённое)
Сущность помечена для удаления. При следующем flush Hibernate выполнит DELETE.
Order order = em.find(Order.class, 1L); // managed
em.remove(order); // помечена как removed
// при flush → DELETE FROM orders WHERE id = 1
Dirty checking: откуда берётся UPDATE без явного save
Одна из часто удивляющих возможностей Hibernate — dirty checking (обнаружение изменений). В момент flush Hibernate сравнивает текущее состояние каждой managed-сущности со снимком, который он сделал при её загрузке. Если поля изменились — автоматически генерируется UPDATE.
Коротко: managed-сущность = живая связь с базой, а не просто объект в памяти.
@Transactional
public void updateEmail(Long userId, String newEmail) {
User user = em.find(User.class, userId); // Hibernate сохраняет снимок
user.setEmail(newEmail); // изменяем поле
// em.save() нет и не нужен
// при flush: UPDATE users SET email = ? WHERE id = ?
}
Dirty checking работает только для managed-сущностей. Для transient и detached Hibernate никакого UPDATE не делает.
Flush: когда изменения уходят в базу
Flush — это синхронизация состояния persistence context с базой данных. Hibernate выполняет SQL, но транзакция ещё не зафиксирована — откат остаётся возможным.
По умолчанию flush происходит:
- перед
commitтранзакции, - перед выполнением JPQL/HQL-запроса, если есть ожидающие изменения для тех же таблиц,
- при явном вызове
em.flush().
@Transactional
public void transfer(Long fromId, Long toId, int amount) {
Account from = em.find(Account.class, fromId);
Account to = em.find(Account.class, toId);
from.deduct(amount);
to.add(amount);
// flush + commit при выходе из транзакции
// → два UPDATE в одной транзакции
}
Ручной em.flush() нужен редко — обычно только когда после изменения сущности нужно сразу выполнить нативный SQL-запрос к той же таблице.
Типичные ошибки, связанные с состояниями
LazyInitializationException — попытка обратиться к lazy-коллекции вне persistence context (после закрытия транзакции). Сущность стала detached, Hibernate не может выдать прокси-запрос в базу.
// НЕПРАВИЛЬНО: транзакция закрылась, items — detached lazy-коллекция
Order order = orderService.findById(1L);
order.getItems().size(); // LazyInitializationException
Решение — загрузить данные внутри транзакции (JOIN FETCH, @EntityGraph) или использовать DTO-проекции. Подробнее — в статье /hibernate/lazy-vs-eager/.
Случайный UPDATE из-за dirty checking — вы загружаете сущность для чтения, но случайно меняете поле — и Hibernate пишет UPDATE. Для read-only операций используйте em.detach(entity) после загрузки или аннотируйте метод @Transactional(readOnly = true) (Spring выставит FlushMode.MANUAL).
Связь со Spring Data JPA
Если вы работаете через JpaRepository, всё описанное работает точно так же — EntityManager просто скрыт внутри Spring Data. Метод save() репозитория вызывает em.persist() для новых сущностей и em.merge() для detached. Managed-сущности, изменённые внутри транзакции, Spring Data не нужно сохранять явно. Подробнее об устройстве репозиториев — в /spring/data-jpa/.
Коротко
- Persistence context — рабочая область Hibernate, живёт в рамках транзакции.
- Identity map гарантирует: одна строка таблицы = один Java-объект в рамках одного persistence context.
- Четыре состояния сущности: transient (не известна Hibernate), managed (под наблюдением), detached (знакома, но не отслеживается), removed (помечена для удаления).
- Dirty checking: Hibernate сам генерирует
UPDATE, если managed-сущность изменилась — явныйsaveне нужен. - Flush синхронизирует изменения с базой перед коммитом или перед запросом к той же таблице.
LazyInitializationException— признак обращения к lazy-данным после закрытия persistence context.
Что почитать дальше
- Ленивая и жадная загрузка — как Hibernate решает, когда идти в базу за связанными данными.
- Транзакции и блокировки — оптимистичные и пессимистичные блокировки,
@Version. - Частые ошибки при работе с Hibernate — N+1, случайные UPDATE, проблемы с каскадами.
- Spring Data JPA — репозитории и как они работают поверх
EntityManager.