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.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ