Вопрос 8 · Раздел 16

Что такое состояния transient, persistent, detached, removed

Четыре термина описывают статус взаимодействия Java-объекта с механизмом ORM (Hibernate/JPA) в конкретный момент времени. Правильное понимание этих состояний критично для предот...

Версии по языкам: English Russian Ukrainian

Обзор

Четыре термина описывают статус взаимодействия Java-объекта с механизмом ORM (Hibernate/JPA) в конкретный момент времени. Правильное понимание этих состояний критично для предотвращения утечек памяти, некорректных обновлений данных и ошибок выполнения.


🟢 Junior Level

4 состояния Entity

Состояние В БД? Отслеживается Hibernate?
Transient ❌ Нет ❌ Нет
Persistent ✅ Да ✅ Да
Detached ✅ Да ❌ Нет
Removed ✅ (помечен на удаление) ✅ (до flush)

Пример каждого состояния

// 1. Transient — новый объект, не в БД
User user = new User();
user.setName("John");
// user НЕ в БД, Hibernate НЕ следит за ним

// 2. Persistent — сохранён и управляется Hibernate
entityManager.persist(user);
// user в БД (INSERT выполнен или запланирован)
// Hibernate следит за изменениями (dirty checking)

// 3. Detached — запись в БД есть, но Hibernate не следит
entityManager.close();
// user всё ещё существует в памяти
// user всё ещё существует в БД
// Но Hibernate больше не отслеживает изменения

// 4. Removed — помечен на удаление
User user2 = entityManager.find(User.class, 1L);
entityManager.remove(user2);
// user2 помечен на удаление
// При flush/commit будет выполнен DELETE

Как проверить состояние

// Проверить является ли entity управляемым
boolean isManaged = entityManager.contains(user);

// Проверить по ID
// Это работает при GenerationType.AUTO/IDENTITY.
// При Assigned generation ID может быть установлен вручную.
if (user.getId() == null) {
    // скорее всего Transient
} else if (entityManager.contains(user)) {
    // Persistent
} else {
    // Detached (есть ID, но не managed)
}

🟡 Middle Level

Когда НЕ использовать dirty checking

Dirty checking удобен для единичных обновлений. При массовой обработке (1000+ сущностей) overhead растёт — используйте native SQL queries или StatelessSession.

Операции перехода между состояниями

persist()  — Transient → Persistent
merge()    — Detached → Persistent (возвращает новую managed копию)
remove()   — Persistent → Removed
refresh()  — reload из БД (обновляет Persistent)
detach()   — Persistent → Detached (evict() в Hibernate)
clear()    — все Persistent → Detached

Детальное поведение каждой операции

persist()

User user = new User();
user.setName("John");
entityManager.persist(user);

// user стал Persistent
// INSERT будет выполнен при flush/commit
user.setName("Jane");  // dirty checking — будет UPDATE

merge()

// merge — самая сложная для понимания операция
User detached = getDetachedUser();  // где-то получен detached объект

User managed = entityManager.merge(detached);

// Что происходит внутри:
// 1. Hibernate ищет entity с таким ID в текущей сессии
// 2. Если не находит — загружает из БД
// 3. Копирует состояние из detached в managed
// 4. Возвращает managed объект
// 5. detached остаётся detached!

detached.setName("Changed");     // НЕ повлияет на БД
managed.setName("Changed");      // повлияет (dirty checking)

SELECT нужен чтобы Hibernate проверил: (1) существует ли запись в БД, (2) загрузил текущее состояние для dirty checking. Без SELECT Hibernate не знает что обновлять.

detach() / evict()

User user = entityManager.find(User.class, 1L);  // Persistent
entityManager.detach(user);  // user → Detached
// user.getId() всё ещё = 1
// Но изменения больше не отслеживаются

// Hibernate-специфичный аналог:
Session session = entityManager.unwrap(Session.class);
session.evict(user);

refresh()

User user = entityManager.find(User.class, 1L);
// Другая транзакция изменила user в БД
entityManager.refresh(user);  // перезагрузить из БД
// Теперь user имеет актуальные значения

Типичные ошибки

// ❌ Detached entity passed to persist
User user = new User();
user.setId(1L);  // ID установлен вручную
entityManager.persist(user);  // ❌ EntityExistsException

// ✅ Правильно
User merged = entityManager.merge(user);

// ❌ Игнорирование return value merge()
entityManager.merge(detached);  // результат потерян!
detached.setName("Changed");    // НЕ сохранится

// ✅ Правильно
User managed = entityManager.merge(detached);
managed.setName("Changed");  // сохранится

// ❌ merge() для persistent
User user = entityManager.find(User.class, 1L);  // уже persistent
entityManager.merge(user);  // лишний SELECT!

🔴 Senior Level

Внутренняя реализация

Структура Persistence Context:

EntityManagerImpl {
    persistenceContext: StatefulPersistenceContext {
        entitiesByKey: Map<EntityKey, Object>,
        entityEntries: Map<Object, EntityEntry>,
    }
}

EntityEntry хранит:
- loadedState — состояние при загрузке (snapshot)
- status — MANAGED, DELETED, READ_ONLY, etc.
- id — identifier
- version — version для optimistic locking
- lazyPropertiesAreUninitialized — флаг lazy загрузки

Identity Guarantee

В рамках одной сессии Hibernate гарантирует:
- Для одного ID в БД существует ровно один объект в памяти
- entityManager.find(User.class, 1L) всегда вернёт тот же объект

User user1 = entityManager.find(User.class, 1L);
User user2 = entityManager.find(User.class, 1L);
assert user1 == user2;  // true (identity)

Это важно для:
-_dirty checking (один объект отслеживается один раз)
- консистентности данных в рамках транзакции

Senior-инсайт: Магия Merge

Когда вы вызываете em.merge(detachedObject):

1. Проверка: есть ли entity с таким ID в persistence context?
   → Да: копируем состояние из detached в managed
   → Нет: загружаем из БД (SELECT)

2. Если в БД нет — создаём новый managed (INSERT при flush)

3. Копируем все поля из detachedObject в managed

4. Возвращаем managed объект

5. detachedObject остаётся detached!
   Все дальнейшие действия — с returned object!

Важно: merge() может вызвать SELECT перед INSERT/UPDATE

Оптимизация для больших данных

@Transactional
public void batchImport(List<User> users) {
    int batchSize = 50;
    for (int i = 0; i < users.size(); i++) {
        entityManager.persist(users.get(i));

        if (i % batchSize == 0 && i > 0) {
            entityManager.flush();    // сбросить в БД
            entityManager.clear();    // очистить persistence context
        }
    }
}

Best Practices

✅ Понимайте состояния сущностей
✅ persist() для новых
✅ merge() для detached (используйте return value!)
✅ Избегайте долгого хранения detached объектов
✅ Используйте entityManager.contains() для проверки
✅ clear() для больших пакетных операций

❌ persist() с установленным ID
❌ merge() для persistent (лишний SELECT)
❌ Игнорирование return value merge()
❌ Доступ к lazy полям detached объектов
❌ Передача detached объектов между потоками

Сравнительная таблица

Состояние Есть в БД? В Persistence Context? Dirty Checking?
Transient Нет Нет Нет
Persistent Да Да Да
Detached Да Нет Нет
Removed Ещё да Да (до flush) Нет

🎯 Шпаргалка для интервью

Обязательно знать:

  • Transient: новый объект, не в БД, Hibernate не следит
  • Persistent: в БД, в persistence context, dirty checking работает
  • Detached: в БД, но НЕ в persistence context, изменения не отслеживаются
  • Removed: помечен на удаление, будет DELETE при flush
  • persist() — Transient → Persistent, merge() — Detached → Persistent (возвращает копию!)
  • entityManager.contains() проверяет является ли сущность managed
  • Identity guarantee: в одной сессии один ID = один объект

Частые уточняющие вопросы:

  • Как проверить состояние? ID null → Transient, contains() true → Persistent, contains() false + ID есть → Detached
  • Почему merge() возвращает новый объект? Оригинальный detached остаётся detached, Hibernate создаёт managed копию
  • Что такое Identity Guarantee? В рамках одной сессии Hibernate гарантирует один ID = один объект в памяти
  • Когда detach() нужен? Для освобождения памяти, при batch-операциях, при обработке больших данных

Красные флаги (НЕ говорить):

  • «persist() для объекта с установленным ID» — EntityExistsException
  • «merge() для persistent — нормально» — лишний SELECT
  • «Detached объекты можно изменять напрямую» — нужны merge()
  • «Передаю detached объекты между потоками» — не thread-safe

Связанные темы:

  • [[7. Опишите жизненный цикл Entity в Hibernate]]
  • [[14. В чём разница между persist() и merge()]]
  • [[12. Что такое dirty checking в Hibernate]]
  • [[15. Что делает метод refresh()]]