Питання 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, тощо.
- 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()]]