Що таке стани transient, persistent, detached, removed
Чотири терміни описують статус взаємодії Java-об'єкта з механізмом ORM (Hibernate/JPA) в конкретний момент часу. Правильне розуміння цих станів критичне для запобігання витоку п...
Огляд
Чотири терміни описують статус взаємодії 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()]]