JavaRush /Курсы /Hibernate deep-dive /Карта состояний сущности

Карта состояний сущности

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

1. Введение

Если вы хоть раз думали: «Я же изменил объект, почему в базе ничего не поменялось?» или, наоборот, «Я же ничего не сохранял, откуда взялся UPDATE?», значит, вы уже столкнулись с базовым правилом Hibernate: ему важен не просто объект, а состояние объекта относительно persistence context. Пока в голове нет чёткой карты этих состояний, ORM и правда выглядит как слегка капризная магия.

В этой лекции мы соберём простую и очень практичную модель: любой объект-сущность, например Product, в каждый момент времени находится в одном из четырёх базовых состояний. Эти состояния определяются не «аннотациями», не «наличием id» и даже не тем, что объект просто существует в памяти, а тем, связан ли он с текущим persistence context и что Hibernate обязан или не обязан с ним делать.

Одного SQL-лога здесь недостаточно: он показывает, что Hibernate сделал, но не объясняет, почему один объект вообще не участвует в работе ORM, другой внезапно приводит к UPDATE, а третий уже выпал из контекста. Чтобы перестать гадать, нужна карта состояний.

Три мира: объект, строка, ORM

Прежде чем разбирать названия состояний, полезно навести порядок в голове: у нас действительно есть три разных мира, и по умолчанию они не совпадают. В Java у вас есть объект в памяти. В PostgreSQL — строка в таблице. А Hibernate пытается держать между ними мост, но этот мост не постоянный и не бесконечный: у него есть границы, правила и жизненный цикл.

Представьте, что вы написали new Product(). Объект появился: можно вызвать setName(), положить его в переменную, передать в метод. Но БД о нём не знает вообще ничего. И — это ключевой момент — Hibernate тоже может о нём ничего не знать, если вы явно не ввели его в persistence context.

Теперь представьте другой случай: вы загрузили Product из базы. Формально это всё тот же «просто Java-объект», но теперь он уже находится под управлением ORM. Если вы меняете его поля внутри транзакции, Hibernate сам решает, когда и как отправить SQL, чтобы синхронизировать изменения.

И вот здесь появляется главная мысль: сущность — это не “класс с аннотацией”, а объект в определённом состоянии относительно persistence context.

2. Четыре состояния: шпаргалка

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

Состояние Объект существует в памяти? Связан с persistence context? Hibernate отслеживает изменения полей? Что обычно с БД?
transient да нет нет строки может не быть (обычно нет)
managed да да да строка может уже быть, а может ещё «планироваться»
detached да нет нет строка обычно есть, но объект живёт отдельно
removed да да да (но уже в режиме удаления) строка помечена к удалению при синхронизации

Обратите внимание на важный нюанс: состояние не отвечает на вопрос «объект существует или нет». Почти всегда он существует как Java-объект, пока на него есть ссылка. Состояние отвечает на другой вопрос: «Hibernate сейчас присматривает за этим объектом или нет?» И если присматривает, то в каком режиме.

Чтобы закрепить картину, давайте нарисуем её в виде диаграммы переходов — очень грубо и без будущих тонкостей:

stateDiagram-v2
    %% Диаграмма переходов между состояниями сущности (очень грубая шпаргалка)
    [*] --> Transient: "new"
    Transient --> Managed: "persist"
    [*] --> Managed: "find / getReference для существующей строки"
    Managed --> Detached: "detach / clear / end of tx"
    Managed --> Removed: "remove"
    Removed --> Detached: "end of tx"

Здесь важно не смешать два разных сценария. persist() переводит в managed новый объект. find() и getReference() не «лечат transient», а дают managed-представление для строки, которая уже существует в базе.

Эта диаграмма не пытается показать весь мир Hibernate, но главное она даёт: объект может «переезжать» между состояниями. Происходят такие переходы либо по вашим явным действиям, например detach() или remove(), либо естественно из-за границ транзакции: закончилась транзакция — закончился persistence context — объект перестал быть managed.

4. transient: объект вне ORM

Состояние transient — это стартовая точка. Оно настолько «обычное», что его часто даже не замечают: вы создали объект через new, и всё. Но для Hibernate это принципиально важный режим, потому что в нём нет никакой ORM-ответственности. Hibernate не обязан его сохранять, не обязан отслеживать изменения, не обязан синхронизировать его с базой. Это просто объект, который живёт по законам Java.

В нашем проекте Commerce Persistence Lab типичный объект в состоянии transient — это новый товар, который вы только собираетесь добавить в каталог. Например, вы сформировали его из каких-то входных данных — неважно, пришли они из UI, теста или seed-скрипта. Пока вы не сказали Hibernate «возьми его под управление», этот объект — как черновик на столе: переписывать его можно сколько угодно, но печатный станок, то есть БД, ещё не получил заказ.

Мини‑пример, мы упрощаем entity до пары полей, чтобы не отвлекаться:

import com.example.commerce.catalog.entity.Product;

// Создаём обычный Java-объект: Hibernate о нём ещё не знает
Product product = new Product();

// Меняем поля в памяти — это НЕ приводит к SQL само по себе
product.setSku("SKU-100");
product.setName("Keyboard");

// product сейчас transient: он есть в памяти, но не в persistence context

Здесь важно не попасть в одну из самых типичных ловушек: наличие или отсутствие id не определяет состояние. Да, у transient-объекта id часто == null, но вы вполне можете руками поставить id или использовать assigned ids, и объект всё равно останется transient, пока не связан с persistence context.

Практический вывод здесь очень простой и без романтики: если объект transient, любые изменения его полей — это просто изменения в памяти. ORM их не видит и ничего никуда отправлять не обязана.

5. managed: объект в persistence context

Состояние managed, его ещё называют persistent, — это режим, в котором Hibernate начинает работать как ORM, а не как библиотека «на полшишечки». Managed означает: объект включён в текущий persistence context. Hibernate знает, что это сущность с конкретной идентичностью, и должен соблюдать правила этого контекста. Самое важное из них — внутри unit of work он начинает отслеживать объект и его изменения.

Объекты в состоянии managed появляются, когда вы получаете сущность из базы через find() или когда регистрируете новый объект через persist(). Сейчас нам важен именно факт перехода в managed, а не вся механика операции. Пока объект managed, Hibernate может применять identity map, сравнивать состояния, готовить SQL на синхронизацию — и делает это не «по настроению», а по внутренним правилам unit of work.

Чтобы не быть голословными, посмотрим минимальный фрагмент Product, в реальном проекте он богаче, но сейчас нам достаточно понять, что это entity:

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;

// Помечаем класс как JPA-сущность — это делает его кандидатом на управление ORM
@Entity
public class Product {

  // Первичный ключ сущности
  @Id
  // IDENTITY: значение id выдаёт база при вставке (и Hibernate подстроится под это)
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;

  // Бизнес-поля (Hibernate будет отслеживать их изменения, когда объект managed)
  private String sku;
  private String name;
}

А теперь пример managed‑объекта на практике:

import com.example.commerce.catalog.entity.Product;
import jakarta.persistence.EntityManager;

// Получаем сущность из БД: внутри активного persistence context она становится managed
Product product = entityManager.find(Product.class, 1L);
// product теперь managed (внутри текущего persistence context)

И вот здесь нужен один важный переключатель в голове: managed-объект — это не просто «данные», а объект, за которым Hibernate следит. Если вы внутри транзакции измените product.setName("New name"), Hibernate уже не может просто «забыть» об этом, потому что в этом и смысл persistence context: он держит управляемый набор объектов и отвечает за их согласованность.

Ещё один нюанс, который часто ломает интуицию новичка: managed не равен «строка уже точно записана в таблицу прямо сейчас». ORM может держать объект как managed и при этом отложить реальный SQL на момент синхронизации. Это нормально. И именно здесь появляется следующий практический вопрос: как новый объект вообще становится managed и почему этот момент не равен мгновенному INSERT.

6. detached: объект вне persistence context

Состояние detached обычно первым вызывает у разработчика когнитивный диссонанс. Объект вроде бы нормальный: у него есть поля, есть getId(), он напечатался в лог, он даже выглядит как «та же сущность». Но Hibernate смотрит на него примерно так: «Да, ты существуешь… но ты не мой». Detached означает: объект не связан с текущим persistence context, поэтому Hibernate не отслеживает его изменения и не обязан синхронизировать их с базой.

В Spring Boot-приложении состояние detached возникает не потому, что вы что-то «сломали», а чаще всего по естественной причине: закончилась транзакция. Persistence context живёт в границе unit of work, обычно это один @Transactional сервисный метод. Метод закончился — контекст закрылся — а ссылки на объекты в вашем коде остались. И эти объекты становятся detached. Поэтому объекты в состоянии detached в обычных приложениях — не редкость, а норма жизни.

Простой пример «ручного» detach, сейчас нам нужен сам факт такого перехода:

import com.example.commerce.catalog.entity.Product;
import jakarta.persistence.EntityManager;

Product product = entityManager.find(Product.class, 1L);

// Явно «отцепляем» объект от persistence context
entityManager.detach(product);

// product теперь detached: объект есть, но Hibernate его не ведёт

В detached-состоянии вы можете делать с объектом что угодно: менять поля, передавать по слоям, сериализовать, хранить в коллекциях. Java это разрешает. Но для ORM это уже не часть текущего unit of work. Поэтому изменения detached-объекта сами по себе не обязаны приводить к SQL.

Если коротко и чуть грубо: managed — это «под присмотром», detached — это «сам по себе». Не плохой и не хороший — просто другой режим.

7. removed: очередь на удаление

Состояние removed многие по ошибке воспринимают как «объект уже удалён». Но Hibernate работает иначе. Когда вы вызываете remove(), объект обычно не исчезает из памяти мгновенно, и это было бы странно: Java так не умеет. Вместо этого Hibernate переводит сущность в специальное состояние: она по‑прежнему находится в persistence context, но уже помечена на удаление при синхронизации с базой.

Removed — это «managed, но с судьбой DELETE». Объект существует, его можно даже прочитать и посмотреть поля, но его жизненный цикл уже предрешён в рамках этой транзакции: при flush или commit будет выполнен DELETE. Точный момент SQL здесь пока не важен; нам важно понять: removed — это отдельное состояние, а не «просто detached» и не «просто transient».

Мини‑пример:

import com.example.commerce.catalog.entity.Product;
import jakarta.persistence.EntityManager;

Product product = entityManager.find(Product.class, 1L);

// Помечаем сущность на удаление: SQL DELETE обычно уйдёт при flush/commit
entityManager.remove(product);

// product теперь removed: он всё ещё в context, но уже помечен на удаление

Removed-состояние особенно важно держать в голове, потому что оно влияет на поведение кода внутри транзакции. Например, вы можете случайно продолжить использовать объект, который уже removed, и удивляться, почему в конце транзакции он «исчез» из базы. Для Java он был жив, а для ORM уже находился «в статусе на вылет».

8. Проверка: EntityManager.contains()

Теория хороша ровно до тех пор, пока вы не пытаетесь отладить реальный сценарий. Поэтому нам нужен самый простой вопрос для проверки: связан ли конкретный объект с текущим persistence context? В JPA для этого есть метод EntityManager.contains(entity). Он не пытается раскрыть все внутренние статусы Hibernate, а отвечает только на одно: знает ли текущий EntityManager именно этот экземпляр.

Важно честно сказать: contains() не превращает вас в волшебника, который видит весь lifecycle сущности. Но для первых экспериментов с картой состояний этого достаточно: метод помогает быстро отделить объект, который всё ещё живёт внутри текущего context, от объекта, который уже существует сам по себе.

Пример маленькой лаборатории, как фрагмент внутри @Transactional метода:

import com.example.commerce.catalog.entity.Product;
import jakarta.persistence.EntityManager;

// Загружаем сущность и проверяем, что она под управлением persistence context
Product p = entityManager.find(Product.class, 1L);

System.out.println(entityManager.contains(p)); // true

// Отцепляем — и сразу видим смену статуса
entityManager.detach(p);

System.out.println(entityManager.contains(p)); // false

Если вы хотите сделать это чуть более «проектно» и не раскидывать println() по коду, можно завести крошечный helper в нашем labsupport-духе. Он не обязателен для бизнеса, но очень удобен для обучения и чтения логов:

import jakarta.persistence.EntityManager;
import org.springframework.stereotype.Component;

// Маленький helper для учебных логов: печатает «под управлением ли объект»
@Component
public class EntityStatePrinter {

  private final EntityManager entityManager;

  // EntityManager внедряется контейнером Spring (внутри активной транзакции он привязан к контексту)
  public EntityStatePrinter(EntityManager entityManager) {
    this.entityManager = entityManager;
  }

  public void printManaged(String label, Object entity) {
    // contains() отвечает только на вопрос «связан ли объект с текущим persistence context»
    System.out.println(label + ": managed=" + entityManager.contains(entity));
    // пример вывода: "Product#1: managed=true"
  }
}

Да, это выглядит как «служба печати правды». И да, в реальном проде вы вряд ли станете этим злоупотреблять. Но в учебной лаборатории такая штука помогает перестать гадать и начать проверять.

И ещё одно наблюдение, которое стоит записать себе крупно: в нашем приложении с короткими транзакциями один и тот же объект может быть managed внутри сервиса и detached сразу после выхода из @Transactional метода. Это не баг и не неожиданность — это нормальная граница unit of work.

9. Типичные ошибки при понимании состояний сущности

Ошибка №1: определять состояние сущности по id != null.
Это очень человеческая ошибка: кажется логичным, что «если id есть — значит объект уже в базе и, значит, managed». Но id — это всего лишь поле. Состояние определяется связью с persistence context. Объект может иметь id и быть detached. А объект может быть managed и при этом находиться в стадии, когда строка в БД ещё не синхронизирована так, как вы ожидаете.

Ошибка №2: думать, что «объект в памяти» и «объект под управлением Hibernate» — одно и то же.
Java‑объект существует, пока на него есть ссылки, и JVM вообще не спрашивает Hibernate, можно ли ему жить дальше. Hibernate же работает только с теми объектами, которые находятся в persistence context. Поэтому «у меня есть объект» не означает «ORM сейчас что-то сделает с ним». Это две разные плоскости: память и ORM-управление.

Ошибка №3: путать detached и removed, потому что в обоих случаях всё выглядит “как обычный объект”.
И detached, и removed — это объекты, которые вы можете держать в переменной и вызывать на них методы. Но detached — это «ORM отпустил». Removed — это «ORM держит, но уже готовит удаление». Если в голове эти состояния смешаны, потом очень трудно объяснять себе и коллегам, почему в конце транзакции улетел DELETE.

Ошибка №4: воспринимать managed как гарантию “SQL уже ушёл”.
Managed — это про управление объектом в контексте, а не про мгновенный SQL. Hibernate часто откладывает запись до момента синхронизации. Если вы начинаете слишком буквально «читать по логам» состояние — увидел persist(), жду INSERT в ту же секунду, — то неизбежно придёте к ощущению, что ORM «иногда работает, иногда нет». На самом деле он работает по правилам unit of work, просто вы пока не держите их в голове.

Ошибка №5: не учитывать, что конец транзакции обычно переводит всё в detached.
Если вы вернули entity из сервисного метода наружу и дальше где-то меняете её поля, ожидая, что Hibernate «подхватит изменения», — вы будете ждать долго, возможно, до следующего отпуска. В приложении с короткими транзакциями объект живёт в managed-режиме только внутри границы транзакции. Вышли наружу — чаще всего detached. Это не плохо, просто важно помнить, где проходит граница ответственности ORM.

1
Задача
Hibernate deep-dive, 2 уровень, 0 лекция
Недоступна
Transient и managed для `Product`
Transient и managed для `Product`
1
Задача
Hibernate deep-dive, 2 уровень, 0 лекция
Недоступна
Detached после закрытия первого context
Detached после закрытия первого context
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ