«1. Обзор

Каждая сущность Hibernate естественным образом имеет жизненный цикл внутри фреймворка — она либо находится в переходном, управляемом, отсоединенном или удаленном состоянии.

Понимание этих состояний как на концептуальном, так и на техническом уровне необходимо для правильного использования Hibernate.

Чтобы узнать о различных методах Hibernate, работающих с сущностями, ознакомьтесь с одним из наших предыдущих руководств.

2. Вспомогательные методы

В этом руководстве мы будем последовательно использовать несколько вспомогательных методов:

    HibernateLifecycleUtil.getManagedEntities(session) — мы будем использовать его для получения всех управляемых объектов из внутреннего хранилища сеанса.
    DirtyDataInspector.getDirtyEntities() — мы собираемся использовать этот метод для получения списка всех сущностей, помеченных как «грязные»
    HibernateLifecycleUtil.queryCount(query) — удобный метод выполнить запрос count(*) к встроенной базе данных

Все вышеперечисленные вспомогательные методы статически импортированы для лучшей читабельности. Вы можете найти их реализации в проекте GitHub, ссылка на который приведена в конце этой статьи.

3. Все дело в контексте персистентности

Перед тем, как перейти к теме жизненного цикла объекта, сначала нам нужно понять контекст персистентности.

Проще говоря, контекст персистентности находится между клиентским кодом и хранилищем данных. Это промежуточная область, где постоянные данные преобразуются в сущности, готовые для чтения и изменения клиентским кодом.

Теоретически контекст персистентности является реализацией шаблона Unit of Work. Он отслеживает все загруженные данные, отслеживает изменения этих данных и отвечает за синхронизацию любых изменений с базой данных в конце бизнес-транзакции.

JPA EntityManager и Hibernate Session являются реализацией концепции контекста персистентности. На протяжении всей этой статьи мы будем использовать Hibernate Session для представления контекста персистентности.

Состояние жизненного цикла объекта Hibernate объясняет, как объект связан с контекстом персистентности, как мы увидим далее.

4. Управляемый объект

Управляемый объект — это представление строки таблицы базы данных (хотя эта строка еще не должна существовать в базе данных).

Это управляется текущим сеансом, и каждое изменение, сделанное в нем, будет отслеживаться и автоматически распространяться в базу данных.

Сеанс либо загружает сущность из базы данных, либо повторно прикрепляет отсоединенную сущность. Мы обсудим отсоединенные объекты в разделе 5.

Давайте посмотрим на код, чтобы получить ясность.

В нашем примере приложения определена одна сущность — класс FootballPlayer. При запуске мы инициализируем хранилище данных некоторыми примерами данных:

+-------------------+-------+
| Name              |  ID   |
+-------------------+-------+
| Cristiano Ronaldo | 1     |
| Lionel Messi      | 2     |
| Gianluigi Buffon  | 3     |
+-------------------+-------+

Допустим, мы хотим изменить имя Буффона для начала — мы хотим ввести его полное имя Джанлуиджи Буффон вместо Джиджи. Буффон.

Во-первых, нам нужно начать нашу единицу работы с получения сеанса:

Session session = sessionFactory.openSession();

В серверной среде мы можем внедрить сеанс в наш код через контекстно-зависимый прокси. Принцип остается прежним: нам нужен сеанс, чтобы инкапсулировать бизнес-транзакцию нашей единицы работы.

Далее мы укажем сеансу загружать данные из постоянного хранилища:

assertThat(getManagedEntities(session)).isEmpty();

List<FootballPlayer> players = s.createQuery("from FootballPlayer").getResultList();

assertThat(getManagedEntities(session)).size().isEqualTo(3);

Когда мы впервые получаем сеанс, его постоянное хранилище контекста пусто, как показано в нашем первом утверждении.

Затем мы выполняем запрос, который извлекает данные из базы данных, создает сущность для данных и, наконец, возвращает сущность для использования.

Внутренне Session отслеживает все объекты, которые он загружает в хранилище постоянного контекста. В нашем случае внутреннее хранилище сеанса будет содержать 3 объекта после запроса.

Теперь давайте изменим имя Джиджи:

Transaction transaction = session.getTransaction();
transaction.begin();

FootballPlayer gigiBuffon = players.stream()
  .filter(p -> p.getId() == 3)
  .findFirst()
  .get();

gigiBuffon.setName("Gianluigi Buffon");
transaction.commit();

assertThat(getDirtyEntities()).size().isEqualTo(1);
assertThat(getDirtyEntities().get(0).getName()).isEqualTo("Gianluigi Buffon");

4.1. Как это работает?

При вызове транзакции commit() или flush() сеанс найдет любые грязные объекты из своего списка отслеживания и синхронизирует состояние с базой данных.

Обратите внимание, что нам не нужно вызывать какой-либо метод, чтобы уведомить Session о том, что мы что-то изменили в нашей сущности — поскольку это управляемая сущность, все изменения автоматически распространяются в базу данных.

«Управляемый объект всегда является постоянным объектом — он должен иметь идентификатор базы данных, даже если представление строки базы данных еще не создано, т. е. оператор INSERT ожидает завершения единицы работы.

См. главу о временных сущностях ниже.

5. Отсоединенная сущность

Отсоединенная сущность — это просто обычная сущность POJO, значение идентификатора которой соответствует строке базы данных. Отличие от управляемого объекта состоит в том, что он больше не отслеживается никаким контекстом сохраняемости.

Сущность может стать отсоединенной, когда сессия, используемая для ее загрузки, была закрыта или когда мы вызываем Session.evict(entity) или Session.clear().

Давайте посмотрим на это в коде:

FootballPlayer cr7 = session.get(FootballPlayer.class, 1L);

assertThat(getManagedEntities(session)).size().isEqualTo(1);
assertThat(getManagedEntities(session).get(0).getId()).isEqualTo(cr7.getId());

session.evict(cr7);

assertThat(getManagedEntities(session)).size().isEqualTo(0);

Наш постоянный контекст не будет отслеживать изменения в отсоединенных объектах:

cr7.setName("CR7");
transaction.commit();

assertThat(getDirtyEntities()).isEmpty();

Session.merge(entity)/Session.update(entity) can (re ) прикрепить сеанс:

FootballPlayer messi = session.get(FootballPlayer.class, 2L);

session.evict(messi);
messi.setName("Leo Messi");
transaction.commit();

assertThat(getDirtyEntities()).isEmpty();

transaction = startTransaction(session);
session.update(messi);
transaction.commit();

assertThat(getDirtyEntities()).size().isEqualTo(1);
assertThat(getDirtyEntities().get(0).getName()).isEqualTo("Leo Messi");

Справку по Session.merge() и Session.update() см. здесь.

5.1. Поле идентификации — это все, что имеет значение

Давайте посмотрим на следующую логику:

FootballPlayer gigi = new FootballPlayer();
gigi.setId(3);
gigi.setName("Gigi the Legend");
session.update(gigi);

В приведенном выше примере мы создали экземпляр объекта обычным способом через его конструктор. Мы заполнили поля значениями и установили идентификатор равным 3, что соответствует идентификатору постоянных данных, принадлежащих Джиджи Буффон. Вызов update() имеет точно такой же эффект, как если бы мы загрузили объект из другого контекста постоянства.

На самом деле Session не различает, откуда возникла повторно присоединенная сущность.

Довольно часто в веб-приложениях создаются отдельные сущности из значений формы HTML.

Что касается сеанса, отсоединенный объект — это просто объект, значение идентификатора которого соответствует постоянным данным.

Имейте в виду, что приведенный выше пример служит только демонстрационной цели. и нам нужно точно знать, что мы делаем. В противном случае мы могли бы получить нулевые значения по всей нашей сущности, если бы просто установили значение в поле, которое хотим обновить, оставив остальные нетронутыми (таким образом, фактически нуль).

6. Временная сущность

Временная сущность — это просто объект сущности, который не имеет представления в постоянном хранилище и не управляется никаким сеансом.

Типичным примером временной сущности может быть создание новой сущности через ее конструктор.

Чтобы сделать временную сущность постоянной, нам нужно вызвать Session.save(entity) или Session.saveOrUpdate(entity):

FootballPlayer neymar = new FootballPlayer();
neymar.setName("Neymar");
session.save(neymar);

assertThat(getManagedEntities(session)).size().isEqualTo(1);
assertThat(neymar.getId()).isNotNull();

int count = queryCount("select count(*) from Football_Player where name='Neymar'");

assertThat(count).isEqualTo(0);

transaction.commit();
count = queryCount("select count(*) from Football_Player where name='Neymar'");

assertThat(count).isEqualTo(1);

Как только мы выполним Session.save(entity), сущность будет назначена значение идентификатора и становится управляемым сеансом. Однако он может быть еще недоступен в базе данных, так как операция INSERT может быть отложена до конца единицы работы.

7. Удаленный объект

Объект находится в удаленном (удаленном) состоянии, если был вызван Session.delete(entity) и сеанс пометил объект для удаления. Сама команда DELETE может быть выполнена в конце единицы работы.

Давайте посмотрим на это в следующем коде:

session.delete(neymar);

assertThat(getManagedEntities(session).get(0).getStatus()).isEqualTo(Status.DELETED);

Однако обратите внимание, что объект остается в постоянном хранилище контекста до конца единицы работы.

8. Заключение

Концепция контекста персистентности является центральной для понимания жизненного цикла сущностей Hibernate. Мы прояснили жизненный цикл, изучив примеры кода, демонстрирующие каждый статус.

Как обычно, код, использованный в этой статье, можно найти на GitHub.