💾 Раздел 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.

Связанные темы