← назад к разделу

Когда вы работаете с 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 или через завершение транзакции.

diagram

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.