Что такое состояния transient, persistent, detached, removed
Четыре состояния сущности (Entity States) определяют взаимоотношения между Java-объектом в памяти JVM и контекстом постоянства JPA (PersistenceContext / сессией Hibernate). От т...
Четыре состояния сущности (Entity States) определяют взаимоотношения между Java-объектом в памяти JVM и контекстом постоянства JPA (PersistenceContext / сессией Hibernate). От текущего состояния объекта зависит, будут ли изменения его полей автоматически отслеживаться ORM, какие SQL-команды будут сгенерированы и в какой момент времени они отправятся в реляционную базу данных.
🟢 Junior Level
4 фундаментальных состояния сущности
| Состояние | Привязан к PersistenceContext? |
Есть строка и ID в БД? | Отслеживается Dirty Checking? | Описание |
|---|---|---|---|---|
Transient (Новый) |
❌ Нет | ❌ Нет | ❌ Нет | Обычный Java-объект, созданный через new. Контекст JPA о нем не знает. |
Persistent (Managed) |
✅ Да | ✅ Да (или будет при flush) | ✅ Да | Сущность управляется сессией. Любое изменение полей попадет в БД. |
Detached (Отсоединенный) |
❌ Нет | ✅ Да | ❌ Нет | Запись в БД есть, но сессия закрыта или сущность отвязана вручную. |
Removed (Удаленный) |
✅ Да (до сброса) | ⚠️ Пока в БД | ❌ Нет | Сущность помечена на удаление. При вызове flush() выполнится DELETE. |
JPA ENTITY STATE MACHINE
[ Transient ] (new User())
│
persist() │ merge()
▼
┌────────────────> [ Persistent ] <────────────────┐
│ │ │ │
│ detach()/clear() │ │ remove() │ merge()
│ ▼ ▼ │
│ [ Detached ] [ Removed ] ────────────┘
│ │ │ persist()
└───────────────────┘ ▼
(SQL DELETE при flush)
Примеры кода для каждого состояния
// 1. TRANSIENT: Объект существует только в Heap, ID равен null
User user = new User();
user.setName("Михаил");
// 2. PERSISTENT (Managed): Привязываем объект к сессии
entityManager.persist(user);
// Теперь user отслеживается сессией.
// Dirty Checking зафиксирует изменение поля без явного вызова update/save!
user.setName("Михаил Обновленный");
// 3. DETACHED: Отсоединяем объект от контекста
entityManager.detach(user);
// user все еще существует в памяти и имеет ID, но Hibernate за ним больше не следит:
user.setName("Иван"); // Это изменение НЕ попадет в БД!
// 4. PERSISTENT (через merge): Возвращаем объект под управление
User managedUser = entityManager.merge(user);
// Внимание: сам user остался Detached, а managedUser — новый Managed-объект!
// 5. REMOVED: Помечаем объект на удаление
entityManager.remove(managedUser);
// При коммите транзакции или entityManager.flush() выполнится SQL DELETE
Как программно проверить состояние сущности
// 1. Проверка, является ли объект управляемым (Managed / Persistent)
boolean isManaged = entityManager.contains(user);
// 2. Эвристическая проверка на Transient / Detached (для GenerationType.IDENTITY/SEQUENCE)
if (user.getId() == null) {
// Объект гарантированно Transient (новый)
} else if (entityManager.contains(user)) {
// Объект Persistent
} else {
// Объект Detached (ID есть, но сессия его не отслеживает)
}
🟡 Middle Level
Матрица операций и переходов состояний
| Исходное состояние | Метод EntityManager |
Результирующее состояние | Поведение и SQL |
|---|---|---|---|
Transient |
persist(e) |
Persistent |
Планирует INSERT. При IDENTITY выполняет INSERT немедленно. |
Transient |
merge(e) |
Persistent (копия) |
Создает новую копию, планирует INSERT, возвращает Managed-копию. |
Persistent |
detach(e) / evict(e) |
Detached |
Исключает сущность из L1-кэша. SQL не генерируется. |
Persistent |
remove(e) |
Removed |
Помечает объект. При сбросе генерирует DELETE. |
Persistent |
refresh(e) |
Persistent |
Выполняет SELECT и затирает изменения в памяти данными из БД. |
Detached |
merge(e) |
Persistent (копия) |
Делает SELECT, копирует поля в Managed-копию, планирует UPDATE. |
Detached |
persist(e) |
❌ Исключение | Бросает EntityExistsException / PersistentObjectException. |
Detached |
remove(e) |
❌ Исключение | Бросает IllegalArgumentException: Removing a detached instance. |
Removed |
persist(e) |
Persistent |
Отменяет удаление, возвращая объект в статус Managed. |
Главная ловушка merge(): игнорирование возвращаемого значения
Наиболее частая ошибка при работе с состояниями:
// Получили detached-объект (например, десериализованный из JSON в контроллере)
User detachedUser = userService.findDetached(1L);
detachedUser.setName("Новое имя");
// ОШИБКА: Возвращаемое значение проигнорировано!
entityManager.merge(detachedUser);
detachedUser.setEmail("new@test.com");
// detachedUser остался в состоянии Detached! Поле email НЕ сохранится в БД!
Анатомия работы merge():
- Hibernate проверяет, есть ли сущность с таким ID в текущем
PersistenceContext. - Если нет — загружает ее из БД через
SELECT(или создает новую, если записи нет). - Копирует состояние всех полей из переданного
detachedUserв найденный/созданныйManagedэкземпляр. - Возвращает ссылку на новый
Managedэкземпляр. - Исходный объект
detachedUserостаетсяDetached.
// ПРАВИЛЬНО:
User managedUser = entityManager.merge(detachedUser);
managedUser.setEmail("new@test.com"); // Сохранится в БД через Dirty Checking
Каскадные переходы состояний (CascadeType)
Аннотация cascade передает смену состояний от родительской сущности к дочерним:
CascadeType.PERSIST: при переводе родителя вPersistentдети также становятсяPersistent.CascadeType.MERGE: вызовmerge()на родителе каскадно выполняетmerge()для всех связанных сущностей.CascadeType.REMOVE: удаление родителя переводит зависимые сущности в состояниеRemoved.CascadeType.DETACH: отсоединение родителя (detach()) отсоединяет и все связанные объекты.
🔴 Senior Level
Внутреннее устройство: StatefulPersistenceContext и EntityEntry
В реализации Hibernate (SessionImpl) контекст постоянства представлен классом StatefulPersistenceContext. Внутри него хранятся две ключевые структуры:
entitiesByKey: Map<EntityKey, Object>— Identity Map (Кэш 1-го уровня). Гарантирует правило: в рамках одной сессии одной строке в БД соответствует ровно один Java-объект.entityEntries: IdentityMap<Object, EntityEntry>— реестр метаданных состояний.
StatefulPersistenceContext
├── entitiesByKey: [ EntityKey(User#1) -> User@0x4a12 ]
└── entityEntries:
└── User@0x4a12 -> EntityEntry {
status: Status.MANAGED,
loadedState: ["Михаил", "misha@mail.com", 30], // Snapshot на момент загрузки
id: 1,
version: 0,
existsInDatabase: true
}
Жизненный цикл EntityEntry:
Status.MANAGED: объект находится вentitiesByKey, массивloadedStateзаполнен начальными значениями колонок.- При вызове
detach()илиclear(): объект удаляется изentitiesByKeyиentityEntries. МассивloadedStateосвобождается сборщиком мусора. - При вызове
remove(): статус вEntityEntryменяется наStatus.DELETED. Объект остается в контексте до фазыflush(). - При вызове
flush(): Hibernate перебираетentityEntries, сравнивает текущие поля Java-объекта со снимкомloadedState(Dirty Checking) и планирует SQL в очереди действийActionQueue.
Проблема assigned ID (ручной установки ID) для Transient/Detached
Если сущность использует ручное присвоение ключа без @GeneratedValue (assigned ID):
@Entity
public class Payment {
@Id
private String transactionUuid; // Assigned ID, задается вручную
private BigDecimal amount;
}
Hibernate не может по полю id == null определить, является ли объект Transient (новым) или Detached (уже существующим в БД).
При вызове merge() или saveOrUpdate() Hibernate вынужден выполнить SQL SELECT, чтобы проверить существование записи:
- Выполняется
SELECT transaction_uuid FROM payment WHERE transaction_uuid = ?. - Если строка найдена — объект считается
Detached(генерируетсяUPDATE). - Если строка не найдена — объект считается
Transient(генерируетсяINSERT).
Production-решение: Реализовать интерфейс Spring Data Persistable<ID> или использовать поле @Version. Если поле @Version == null, сущность однозначно признается Transient без лишнего SELECT.
Пакетная обработка и утечки памяти L1-кэша
Поскольку все Persistent объекты удерживаются сильными ссылками в StatefulPersistenceContext.entityEntries, массовая обработка приводит к OutOfMemoryError:
@Transactional
public void processHugeDataset() {
int batchSize = 50;
for (int i = 0; i < 100_000; i++) {
User user = new User("User_" + i);
entityManager.persist(user);
if (i > 0 && i % batchSize == 0) {
entityManager.flush(); // Сбрасываем накопленные INSERT в базу
entityManager.clear(); // Переводим все объекты в Detached, освобождая кучу
}
}
}
Альтернатива: Использование StatelessSession в Hibernate, которая не создает контекст постоянства, не отслеживает состояния и не реализует кэш L1.
4 Tricky Questions
1. Как Hibernate различает Transient и Detached при вызове merge(), если у сущности нет поля @Version и ID присваивается вручную?
Ответ:
- Если используется
@GeneratedValue, проверка тривиальна:id == nullозначаетTransient,id != null—Detached. - Если ID присвоен вручную (
Assigned ID) и поле@Versionотсутствует, Hibernate выполняет предварительный запросSELECTпо первичному ключу в БД. - Если запись обнаружена в БД — сущность трактуется как
Detachedи объединяется черезUPDATE. - Если запись не обнаружена — сущность трактуется как
Transientи сохраняется черезINSERT. - Чтобы избежать штрафного
SELECT, сущность должна реализоватьorg.springframework.data.domain.Persistable<ID>с методомisNew(), либо иметь версионное поле@Version.
2. Что произойдет, если в методе с @Transactional изменить поле Detached объекта, затем вызвать entityManager.merge(detached), а после продолжить менять исходный Detached объект?
Ответ:
- Изменения, внесенные в
detachedобъект до вызоваmerge(), будут скопированы в возвращенныйManagedобъект и попадут в базу данных. - Изменения, внесенные в исходный
detachedобъект после вызоваmerge(), в базу данных НЕ попадут, так как исходная ссылка так и осталась в состоянииDetached. - В базу данных попадут только те последующие изменения, которые были выполнены над объектом, возвращенным из вызова:
User managed = entityManager.merge(detached); managed.setField(...).
3. Можно ли вернуть объект из состояния Removed обратно в состояние Persistent? Что произойдет?
Ответ:
- Да, спецификация JPA разрешает вызвать
entityManager.persist(removedEntity)до того, как был выполнен сброс контекста (flush()). - Внутренний статус сущности в
EntityEntryменяется сStatus.DELETEDобратно наStatus.MANAGED. - Запланированная операция SQL
DELETEудаляется из очередиActionQueue. - Если же
flush()уже произошел, SQLDELETEуже отправлен в БД, и повторный вызов приведет либо к ошибке целостности, либо к повторной вставке в зависимости от генератора ключей.
4. Почему вызов entityManager.lock(detachedEntity, LockModeType.OPTIMISTIC) выбрасывает исключение, а merge() работает?
Ответ:
- Блокировки (как пессимистичные, так и оптимистичные) могут накладываться только на управляемые сущности (
Managed), находящиеся в активномPersistenceContext. - Попытка наложить блокировку на объект в состоянии
Detachedнемедленно завершается исключениемIllegalArgumentException: entity not managed. - Чтобы заблокировать
Detachedобъект, его необходимо предварительно объединить с сессией черезUser managed = entityManager.merge(detachedUser)и только затем передаватьmanagedвentityManager.lock().
🎯 Шпаргалка для интервью
30-секундный ответ (Elevator Pitch)
«В JPA существует 4 состояния сущности:
Transient(новый POJO, не привязан к сессии, нет ID),Persistent(привязан кPersistenceContext, отслеживается Dirty Checking, синхронизируется с БД),Detached(имеет ID в БД, но сессия закрыта или очищена вызовомdetach()/clear()), иRemoved(помечен на удаление при ближайшемflush). Ключевой метод возврата из Detached в Persistent —merge(), который не мутирует исходный объект, а загружает и возвращает управляемую копию. При работе с большими объемами данных необходимо регулярно вызыватьflush()иclear(), переводя сущности вDetached, чтобы избежать переполнения памяти первого уровня кэша».
Ключевые метрики для Production-мониторинга
- Hibernate Session Size (
Session.getStatistics().getEntityCount()): резкий рост указывает на отсутствие очистки контекста (clear()) в пакетных процессах. - Длительность фазы
flush: время, затрачиваемое Dirty Checking на обход картыentityEntries. - Количество штрафных SELECT перед MERGE: выявляет проблемы с отсутствием
isNew()/@Versionу сущностей с assigned ID.
Красные флаги (Чего говорить НЕЛЬЗЯ)
- ❌ «Метод merge() делает исходный переданный объект управляемым» — Грубейшая ошибка. Исходный объект остается detached, возвращается новая копия.
- ❌ «Detached-объект можно удалить напрямую через entityManager.remove(detached)» — Бросит
IllegalArgumentException. Сначала требуетсяfind/getReferenceилиmerge. - ❌ «Сущность в состоянии Transient имеет ID в базе данных» — Нет, transient-объект еще не сохранялся и ID не имеет (кроме ручного присвоения UUID).
- ❌ «Dirty checking работает для Detached объектов» — Нет, изменения в Detached объектах игнорируются до вызова
merge().