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

Що таке EntityManager і чим він відрізняється від Session

Починаючи з версії Hibernate 5.2+, інтерфейс Session напряму наслідує EntityManager:

EntityManager та Session — це два основні інтерфейси для взаємодії застосунку з об’єктно-реляційним відображенням (ORM) у Java. EntityManager є офіційним стандартом специфікації JPA (Jakarta Persistence API), тоді як Session — це історичний, вендор-специфічний інтерфейс фреймворку Hibernate, що розширює стандартний функціонал потужними низькорівневими можливостями.


🟢 Junior Level

Що таке EntityManager та Session

  • EntityManager (JPA): Стандартний інтерфейс Java EE / Jakarta EE (jakarta.persistence.EntityManager). Він визначає єдиний платформонезалежний контракт роботи із сутностями, який зобов’язаний підтримувати будь-який JPA-сумісний провайдер (Hibernate, EclipseLink, OpenJPA).
  • Session (Hibernate): Власний інтерфейс бібліотеки Hibernate (org.hibernate.Session), що з’явився задовго до першої специфікації JPA.

Починаючи з версії Hibernate 5.2+, інтерфейс Session напряму наслідує EntityManager:

package org.hibernate;

public interface Session extends jakarta.persistence.EntityManager, AutoCloseable {
    // Додаткові фірмові методи Hibernate
}
                     ІЄРАРХІЯ ІНТЕРФЕЙСІВ ORM
                     
            ┌────────────────────────────────────────┐
            │  jakarta.persistence.EntityManager    │ (JPA Стандарт)
            └───────────────────▲────────────────────┘
                                │ extends
            ┌───────────────────┴────────────────────┐
            │        org.hibernate.Session           │ (Hibernate API)
            └───────────────────▲────────────────────┘
                                │ implements
            ┌───────────────────┴────────────────────┐
            │    org.hibernate.internal.SessionImpl  │ (Реалізація)
            └────────────────────────────────────────┘

Як перейти від JPA до можливостей Hibernate

Якщо в застосунку використовується стандартний EntityManager, отримати доступ до вендорського API Hibernate можна в один рядок через метод unwrap():

@PersistenceContext
private EntityManager entityManager;

public void demonstrateUnwrap() {
    // Витягуємо нативну сесію Hibernate:
    Session session = entityManager.unwrap(Session.class);
    
    // Тепер доступні фірмові методи Hibernate
    boolean connected = session.isConnected();
}

Порівняльна таблиця методів

Завдання Стандарт JPA (EntityManager) Нативний Hibernate (Session)
Збереження нової сутності persist(entity) (повертає void) persist(entity) або legacy save(entity) (повертає Serializable ID)
Злиття від’єднаної сутності merge(entity) merge(entity) або legacy update() / saveOrUpdate()
Видалення сутності remove(entity) remove(entity) або legacy delete(entity)
Пошук за первинним ключем find(Class, id) find(Class, id) або get(Class, id)
Отримання лінивого посилання (Proxy) getReference(Class, id) load(Class, id)
Очищення контексту clear() clear()
Від’єднання сутності detach(entity) detach(entity) або legacy evict(entity)

🟡 Middle Level

Ексклюзивні можливості Session, відсутні в JPA

Хоча EntityManager покриває 95% типових завдань вебзастосунку, Session надає унікальні інструменти оптимізації:

1. Пакетне мульти-завантаження за списком ID (multiLoad)

Стандартний JPA для завантаження пачки сутностей вимагає написання JPQL: SELECT u FROM User u WHERE u.id IN (:ids). Цей запит завжди йде в базу даних, ігноруючи кеш L1/L2. Hibernate Session надає інтелектуальний метод byMultipleIds():

Session session = entityManager.unwrap(Session.class);

List<Long> ids = List.of(1L, 2L, 3L, 4L, 5L);
List<User> users = session.byMultipleIds(User.class)
    .enableCheckCache(true) // Перевіряє L1 і L2 кеш перед відправкою SQL!
    .multiLoad(ids);

Якщо сутності з ID 1 та 2 вже лежать у кеші L1, Hibernate сформує SQL суворо для решти: WHERE id IN (3, 4, 5).

2. Прямий доступ до JDBC Connection (doWork)

Коли потрібно виконати складний низькорівневий JDBC-код або викликати специфічні процедури СУБД:

Session session = entityManager.unwrap(Session.class);

session.doWork(connection -> {
    try (PreparedStatement ps = connection.prepareStatement("COPY users TO STDOUT")) {
        // Пряма робота з сокетом PostgreSQL / Oracle без накладних витрат ORM
    }
});

3. Динамічні фільтри даних (Hibernate Filters)

Дозволяють глобально увімкнути умову фільтрації (наприклад, Soft Delete або Multi-tenancy) на рівні всієї сесії:

Session session = entityManager.unwrap(Session.class);
session.enableFilter("tenantFilter").setParameter("tenantId", "EU_REGION");

// Тепер АБСОЛЮТНО ВСІ вибірки автоматично отримують умову WHERE tenant_id = 'EU_REGION'
List<Order> orders = entityManager.createQuery("FROM Order", Order.class).getResultList();

🔴 Senior Level

Потокобезпека та архітектура Spring: SharedEntityManagerCreator

І EntityManager, і Session категорично не потокобезпечні. Вони інкапсулюють стан відкритого JDBC-з’єднання та локальну карту PersistenceContext (L1 Cache).

Проте в Spring-сервісах ми спокійно інжектуємо EntityManager як поле в Singleton-бін:

@Service
public class UserService {
    @PersistenceContext
    private EntityManager entityManager; // Як це працює в багатопотоковому середовищі?!
}

Внутрішня архітектура Spring:

  1. Поле entityManager містить НЕ реальний SessionImpl, а динамічний проксі-об’єкт, створений класом SharedEntityManagerCreator.
  2. При виклику будь-якого методу (наприклад, entityManager.find()) проксі-обробник (SharedEntityManagerInvocationHandler):
    • Перевіряє TransactionSynchronizationManager поточного потоку (ThreadLocal).
    • Витягує реальний EntityManager, прив’язаний до поточної активної транзакції @Transactional.
    • Якщо транзакція не відкрита, проксі створює тимчасовий EntityManager, виконує операцію і відразу закриває його.
  3. Таким чином, кожен паралельний потік вебсервера працює зі своїм власним ізольованим екземпляром SessionImpl.
                    ІНЖЕКЦІЯ ENTITYMANAGER У SPRING
                    
   [ Потік HTTP-1 ] ──┐
                      ▼
     ┌────────────────────────────────────────────────────────┐
     │  Singleton Proxy (SharedEntityManagerInvocationHandler)│
     └────────────────────────────────────────────────────────┘
                      │ 
       ThreadLocal    ├──> [ Thread 1: Реальний SessionImpl @0x123 ]
       Resolution     │
                      └──> [ Thread 2: Реальний SessionImpl @0x456 ]

Потокова обробка величезних вибірок: ScrollableResults

Спроба завантажити 1 000 000 рядків через стандартний JPA getResultList() призведе до негайного OutOfMemoryError, оскільки JPA намагається матеріалізувати весь список у пам’ять.

Hibernate Session вирішує це завдання за допомогою курсорного стрімінгу:

Session session = entityManager.unwrap(Session.class);

try (ScrollableResults<User> scroll = session.createQuery("FROM User", User.class)
        .setFetchSize(100) // Читаємо пачками по 100 рядків з курсора БД
        .scroll(ScrollMode.FORWARD_ONLY)) {

    int count = 0;
    while (scroll.next()) {
        User user = scroll.get();
        process(user);

        if (++count % 100 == 0) {
            session.flush();
            session.clear(); // Очищаємо L1 кеш для запобігання OOM
        }
    }
}

4 Tricky Questions

1. Чи є Session обгорткою (декоратором) над EntityManager, чи навпаки? Який їхній зв’язок в Hibernate 6?

Відповідь:

  1. Жодних зовнішніх декораторів немає. Починаючи з Hibernate 5.2, базовий інтерфейс org.hibernate.Session прямо розширює jakarta.persistence.EntityManager.
  2. Клас SessionImpl, який їх реалізує, одночасно є і повноцінним EntityManager, і Session.
  3. Тому виклик entityManager.unwrap(Session.class) не створює новий об’єкт-обгортку, а виконує звичайне безпечне приведення типів (Downcasting) внутрішнього посилання до інтерфейсу Session.

2. У чому принципова різниця між session.save(entity) та entityManager.persist(entity) для сутності зі стратегією генерації ключа SEQUENCE?

Відповідь:

  1. entityManager.persist(entity) повертає void. При виклику він запитує чергове число з послідовності СУБД, призначає ID об’єкту в пам’яті та відкладає виконання SQL INSERT до фази flush().
  2. Застарілий метод session.save(entity) має сигнатуру Serializable save(Object object). Він зобов’язаний негайно повернути ідентифікатор коду, що його викликав.
  3. Хоча обидва методи запитують послідовність без негайного INSERT, метод save() не входить до специфікації JPA і не враховує деякі тонкощі життєвого циклу каскадних транзакцій JPA. У сучасному коді виклик session.save() визнаний застарілим (deprecated), замість нього слід використовувати persist().

3. Чому прямий виклик sessionFactory.getCurrentSession() у застосунку без Spring Framework може викинути HibernateException: No CurrentSessionContext configured?

Відповідь:

  1. Методу getCurrentSession() потрібна стратегія прив’язки сесії до контексту виконання (до потоку або транзакції JTA).
  2. За це відповідає властивість hibernate.current_session_context_class.
  3. У чистому Hibernate без Spring потрібно вручну сконфігурувати значення thread (прив’язка до ThreadLocal) або jta.
  4. У Spring Framework налаштування виконується автоматично: Spring реєструє власний клас SpringSessionContext, який синхронізує Hibernate Session з менеджером транзакцій PlatformTransactionManager.

4. Що станеться, якщо закрити Session, отриману через entityManager.unwrap(Session.class).close(), всередині методу з анотацією @Transactional?

Відповідь:

  1. Оскільки в Spring EntityManager керується інфраструктурою транзакцій, виклик session.close() примусово закриє фізичне JDBC-з’єднання та очистить контекст персистентності.
  2. При виході з методу перехоплювач @Transactional спробує виконати фазу коміту (flush() та commit()).
  3. Оскільки сесія вже закрита, Spring викине виняток: TransactionSystemException: Could not commit JPA transaction; nested exception is org.hibernate.SessionException: Session is closed!.
  4. Правило: Ніколи не закривати вручну EntityManager або Session, впроваджені через Spring!

🎯 Шпаргалка для інтерв’ю

30-секундна відповідь (Elevator Pitch)

«EntityManager — це стандартний переносний JPA-інтерфейс, визначений специфікацією Jakarta EE. Session — це нативний інтерфейс Hibernate, який у сучасних версіях (Hibernate 5.2+) напряму наслідує EntityManager. У стандартних сценаріях рекомендується завжди використовувати EntityManager заради дотримання абстракції та переносності коду. Доступ до Session отримують через entityManager.unwrap(Session.class) лише тоді, коли потрібні специфічні можливості Hibernate: пакетне завантаження за кількома ID (multiLoad), пряме керування JDBC через doWork(), потоковий курсор ScrollableResults або динамічні Hibernate Filters».

Ключові метрики для Production-моніторингу

  • Hibernate Session Open / Close Rate: співвідношення сесій, що відкриваються та закриваються (гарантія відсутності витоків з’єднань).
  • Active Transactions per Thread: контроль коректної ізоляції сесій у пулі потоків.

Червоні прапорці (Чого говорити НЕ МОЖНА)

  • ❌ «Session — це застарілий інтерфейс, який більше не використовується всередині Hibernate» — Неправда. Вся внутрішня реалізація ядра Hibernate (SessionImpl) побудована навколо Session.
  • ❌ «EntityManager потокобезпечний, тому що ми впроваджуємо його через @Autowired або @PersistenceContext у Singleton-біни» — Сам EntityManager суворо однопотоковий; потокобезпеку забезпечує транзакційний проксі Spring (SharedEntityManagerCreator).
  • ❌ «Потрібно завжди закривати Session вручну через session.close() наприкінці методу сервісу» — У Spring це зламає транзакцію і викличе Session is closed.
  • ❌ «Session огортає EntityManager через патерн Wrapper» — Ні, в Hibernate Session наслідує EntityManager.

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