Що таке стани 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, звільняючи купу (heap)
}
}
}
Альтернатива: Використання 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().