Питання 7 · Розділ 16

Опишіть життєвий цикл Entity в Hibernate

Кожна сутність в Hibernate проходить через певні стани (життєвий цикл) в процесі взаємодії з базою даних. Розуміння життєвого циклу критично важливе для правильного використання...

Мовні версії: English Russian Ukrainian

Огляд

Кожна сутність в Hibernate проходить через певні стани (життєвий цикл) в процесі взаємодії з базою даних. Розуміння життєвого циклу критично важливе для правильного використання Hibernate та запобігання помилкам.

Навіщо це знати: щоб розуміти який метод викликати (persist чи merge), чому зміни зберігаються самі (dirty checking), чому виникає «detached entity passed to persist».


🟢 Junior Level

4 стани Entity

Сутність в Hibernate може перебувати в одному з чотирьох станів:

  1. Transient (Новий / Тимчасовий) — створений через new, ще не в БД
  2. Persistent (Керований) — збережений в БД, відстежується Hibernate
  3. Detached (Від’єднаний) — був в БД, але сесія закрита
  4. Removed (Видалений) — помічений на видалення, буде видалений при flush
new User() ──persist()──→ Managed ──close()──→ Detached
                             │
                          remove()
                             ↓
                          Removed

Приклад

// 1. Transient — новий об'єкт, не в БД
User user = new User();
user.setName("John");

// 2. Persistent — збережений, відстежується
entityManager.persist(user);
// Тепер user в БД і під керуванням Hibernate

// 3. Detached — сесія закрита
entityManager.close();
// user все ще існує в пам'яті, але Hibernate більше не стежить

// 4. Removed — помічений на видалення
entityManager.remove(user);
// При flush/commit буде виконаний DELETE

🟡 Middle Level

persist() vs merge()

Операція Transient Detached Persistent
persist() INSERT ПОМИЛКА ПОМИЛКА
merge() SELECT+INSERT SELECT+UPDATE SELECT (марно)

Переходи між станами

Transient → Persistent:  persist(), merge()
Persistent → Detached:   close(), clear(), detach()/evict()
Detached → Persistent:   merge(), update()
Persistent → Removed:    remove()
Transient → Detached:    (неможливо напряму)

Детальний опис операцій

// persist() — для нових сутностей (INSERT)
User newUser = new User();
entityManager.persist(newUser);
// newUser тепер Persistent

// merge() — для detached сутностей (UPDATE)
User detached = getDetachedUser();
User managed = entityManager.merge(detached);
// managed — нова керована копія
// detached залишається detached!

// remove() — для видалення (DELETE)
entityManager.remove(managedUser);
// При flush буде виконаний DELETE

// refresh() — перезавантажити з БД
entityManager.refresh(user);
// Оновлює дані з БД, перезаписуючи зміни в пам'яті

// detach()/evict() — від'єднати. evict() — Hibernate-specific (Session). detach() — JPA-стандарт (EntityManager). Використовуйте detach() для переносимості.
entityManager.detach(user);
// user стає Detached

// clear() — від'єднати всі
entityManager.clear();
// Всі сутності стають Detached

Dirty Checking

Dirty checking — Hibernate автоматично відстежує зміни об’єктів. Ви просто змінюєте поле — Hibernate сам робить UPDATE.

@Transactional
public void updateUser(Long id, String name) {
    User user = entityManager.find(User.class, id);  // Persistent
    user.setName(name);
    // НЕ ПОТРІБЕН em.merge() чи em.save()!
    // Hibernate автоматично виявить зміну і зробить UPDATE
}

Типові помилки

// ❌ Detached entity passed to persist
User user = new User();
user.setId(1L);  // ID вже встановлений
entityManager.persist(user);  // ❌ EntityExistsException або помилка

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

// ❌ merge() для нової сутності (зайвий SELECT)
User newUser = new User();
entityManager.merge(newUser);  // Спочатку SELECT, потім INSERT

// ✅ Правильно: persist() для нових
entityManager.persist(newUser);  // Одразу INSERT

// ❌ merge() для persistent (марно)
User user = entityManager.find(User.class, 1L);  // Persistent
User merged = entityManager.merge(user);  // Зайвий SELECT

🔴 Senior Level

Внутрішня реалізація

Persistence Context — контейнер для всіх managed сутностей в рамках EntityManager. EntityEntry — запис всередині Persistence Context, зберігає поточний стан та snapshot. Snapshot — копія стану об’єкта на момент завантаження, використовується для dirty checking.

Persistence Context (StatefulPersistenceContext):

EntityManagerImpl {
    persistenceContext: StatefulPersistenceContext {
        entitiesByKey: Map<EntityKey, Object>,     // cache by ID
        entityEntries: Map<Object, EntityEntry>,   // state tracking
    }
}

EntityEntry зберігає:
- snapshot — стан при завантаженні (для dirty checking)
- status — поточний стан (MANAGED, DELETED, тощо)
- loadedState — стан при завантаженні з БД

Механізм dirty checking

1. При завантаженні: Hibernate зберігає snapshot entity
2. При зміні: змінюється поле в об'єкті
3. При flush: Hibernate порівнює snapshot з поточним станом
4. Якщо різні → генерує UPDATE SQL
5. Оновлює snapshot

Алгоритм:
- O(N) де N = число entities в persistence context
- Для великих контекстів може бути повільно
- Рішення: entityManager.clear() періодично

Управління пам’яттю при великих операціях

@Transactional
public void batchProcess(List<User> users) {
    for (int i = 0; i < users.size(); i++) {
        entityManager.merge(users.get(i));

        // Періодичне очищення persistence context
        if (i % 50 == 0) {
            entityManager.flush();    // скинути в БД
            entityManager.clear();    // очистити кеш
        }
    }
}

Після clear() всі раніше завантажені/створені об’єкти стають detached. Якщо цикл продовжується — наступні ітерації працюють з новими об’єктами, старі втрачаються (і при зверненні до них знадобиться merge()).

Best Practices

✅ persist() для нових сутностей
✅ merge() для detached сутностей
✅ remove() для видалення
✅ Dirty checking для оновлень (не потрібен merge)
✅ entityManager.clear() для пакетних операцій
✅ flush + clear кожні 50-100 entities

❌ persist() для detached (помилка)
❌ merge() для нових (зайвий SELECT)
❌ merge() для persistent (зайвий SELECT)
❌ Ігнорування станів сутностей
❌ Завантаження великої кількості entities без clear

🎯 Шпаргалка для співбесіди

Обов’язково знати:

  • 4 стани: Transient (new), Persistent (managed), Detached (в БД, але не tracked), Removed (помічений на видалення)
  • persist() — для нових (INSERT), merge() — для detached (UPDATE/INSERT)
  • Dirty checking: Hibernate автоматично відстежує зміни managed-сутностей
  • Persistence Context — контейнер для всіх managed сутностей, зберігає snapshot для dirty checking
  • merge() повертає НОВИЙ managed об’єкт, оригінал залишається detached
  • entityManager.clear() очищує persistence context, всі сутності стають detached

Часті уточнюючі запитання:

  • Чому persist() для нових краще merge()? persist() = одразу INSERT, merge() = спочатку SELECT (перевірка)
  • Що таке snapshot? Копія стану при завантаженні, використовується для dirty checking при flush
  • Як обробити 10k+ сутностей? persist/merge + flush + clear кожні 50-100 entities
  • Чому merge() повертає новий об’єкт? Тому що оригінал може бути detached, Hibernate створює managed копію

Червоні прапорці (НЕ говорити):

  • «Використовую merge() для нових сутностей» — зайвий SELECT
  • «Ігнорую return value merge()» — зміни не збережуться
  • «persist() для detached з ID» — EntityExistsException
  • «Не чищу persistence context при batch» — OutOfMemoryError

Пов’язані теми:

  • [[8. Що таке стани transient, persistent, detached, removed]]
  • [[12. Що таке dirty checking в Hibernate]]
  • [[13. Як працює механізм flush в Hibernate]]
  • [[14. У чому різниця між persist() і merge()]]
  • [[9. Що таке кеш першого рівня в Hibernate]]