Что такое StatelessSession в Hibernate и как это использовать?

Интерфейс StatelessSession в Hibernate предоставляет командно-ориентированный API, который дает вам больше контроля над выполняемыми SQL-запросами.

· 5 минуты на чтение

Некоторые из основных возможностей Hibernate - это сравнение состояния объектов (dirty checks), выталкивание контекста хранения (flushes), и кэш первого уровня. Они делают реализацию большинства стандартных сценариев использования простым и эффективным. Но также добавляют много скрытой логики, которая влияет на производительность.

Сценарии, которые выполняют большое количество записи в базу данных, не выиграют от этих возможностей. Например, типичная задача импорта данных больших данных ночью из другой системы с загрузкой в БД. В таких ситуациях стоит подумать об использовании StatelessSession от Hibernate.

Спонсор поста
😺
Данная статья является переводом и адаптацией статьи: "Hibernate’s StatelessSession – What it is and how to use it"

Используемые версии

Java 17
Hibernate 6.1.1
H2 2.1.214

Что такое StatelessSession?

StatelessSession предоставляет командно-ориентированный API, который гораздо ближе к JDBC. Этот функционал доступен только в Hibernate и не включен в JPA.

В этой статье я покажу вам несколько примеров его использования для реализации простых операций чтения и записи. Но прежде давайте поговорим о концептуальных отличиях от стандартного интерфейса Session.

StatelessSession не предоставляет кэш первого уровня, сравнение состояния объектов (dirty checks) или write-behind. Он также не обеспечивает ленивую загрузку связей и не использует кэши второго и третьего уровня. Любая операция также не вызывает никаких событий жизненного цикла или перехватчиков.

Вместо всех функций, которые предоставляет обычный Session или EntityManager в JPA, мы получаем полный контроль над выполняемыми SQL-запросами.

Если вы хотите получить какие-то данные из базы данных или инициализировать связь между сущностями, вам нужно написать и выполнить соответствующий запрос. А если вы создаете новый или изменяете существующий объект сущности, вам нужно вызвать метод insert(), update() или delete() интерфейса StatelessSession, чтобы сохранить изменения.

Это требует от вас большего внимания к технической стороне слоя хранения данных (persistence layer). Но если ваши задачи не требуют сравнение состояния объектов (dirty checks), ленивой загрузки или кэшей первого уровня, использование StatelessSession значительно снижает накладные расходы на производительность, которые добавляют все эти функции.

Это делает StatelessSession отличным решением для случаев, когда импортируется или обновляется огромный набор данных. Вы также можете попробовать StatelessSession, если вам нужно получить много объектов сущностей, которые вы не будете изменять.

Как использовать StatelessSession?

😺
Проект на GitHub: hibernates-statelesssession

Давайте используем StatelessSession для чтения и записи объектов сущностей. Экземпляр StatelessSession можно получить так же, как и обычный экземпляр Session.

Если ваше приложение основано на Spring, вы можете просто внедрить экземпляр StatelessSession. А если вы используете обычный Hibernate, вызовите метод openStatelessSession() вашего SessionFactory и использовать его для начала транзакции.

StatelessSession statelessSession = sessionFactory.openStatelessSession();
statelessSession.getTransaction().begin();
 
// ваши запросы
 
statelessSession.getTransaction().commit();

Вставка и обновление сущностей

Большинство проектов используют StatelessSession для вставки или обновления огромных наборов данных. Поэтому давайте начнем с двух простых операций записи.

Наиболее важные методы, которые вам необходимо знать - это методы insert(), update() и delete(). StatelessSession не поддерживает каскадные операции, поэтому вам нужно вызывать операции записи для каждого объекта сущности, который вы хотите сохранить.

В следующем тестовом примере мы сохраним новую сущность Post, а затем исправим опечатку в поле title.

final StatelessSession statelessSession = sessionFactory.openStatelessSession();

statelessSession.getTransaction().begin();

final Post post = new Post();
post.setTitle("New Past");

statelessSession.insert(post);

post.setTitle("New Post");

statelessSession.update(post);

statelessSession.getTransaction().commit();

Если вы знакомы с интерфейсом Session или EntityManager, то могли уже заметить отличия. Я вызвал метод insert() для сохранения нового объекта Post и метод update() для сохранения измененного поля title.

Без кэша первого уровня и сравнения состояния объектов (dirty checks) Hibernate немедленно выполняет оператор SQL INSERT, когда вы вызываете метод insert(). Также Hibernate самостоятельно не обнаруживает изменений в поле title. Вам нужно вызвать метод update(), чтобы сохранить это изменение. Тогда будет немедленно выполнен оператор SQL UPDATE. Это все можно увидеть в логах:

16:17:57.635 [main] DEBUG org.hibernate.SQL - 
    select
        next value for Post_SEQ
16:17:57.656 [main] DEBUG org.hibernate.SQL - 
    insert 
    into
        Post
        (title, id) 
    values
        (?, ?)
16:17:57.661 [main] DEBUG org.hibernate.SQL - 
    update
        Post 
    set
        title=? 
    where
        id=?

Чтение из базы данных

Я уже упоминал, что StatelessSession не обеспечивает ленивую загрузку. Поэтому при получении объекта сущности из базы данных необходимо самостоятельно инициализировать все необходимые связи. В противном случае вы получите LazyInitializationException при первом обращении к связной сущности.

Лучший способ инициализировать связи - использовать граф сущностей (EntityGraph) или включить предложение JOIN FETCH в запрос JPQL.

В следующих примерах я использую запрос JPQL с JOIN FETCH для загрузки объекта сущности PostComments. Это позволит нам загрузить комментарии для постов.

final StatelessSession statelessSession = sessionFactory.openStatelessSession();

statelessSession.getTransaction().begin();

final Post post = statelessSession.createQuery("""
                SELECT p
                FROM Post p
                    JOIN FETCH p.comments
                WHERE p.id=:id""", Post.class)
        .setParameter("id", 1L)
        .getSingleResult();

log.info(post.getId() + " " + post.getTitle());
log.info("Comments size: " + post.getComments().size());

statelessSession.getTransaction().commit();
17:07:07.115 [main] DEBUG org.hibernate.SQL - 
    select
        p1_0.id,
        c1_0.post_id,
        c1_0.id,
        c1_0.review,
        p1_0.title 
    from
        Post p1_0 
    join
        PostComment c1_0 
            on p1_0.id=c1_0.post_id 
    where
        p1_0.id=?
17:07:07.588 [main] INFO  d.s.e.h.statelesssession.Main - 1 Пост 0
17:07:07.604 [main] INFO  d.s.e.h.statelesssession.Main - Comments size: 10000

Так как Hibernate не использует никакие кэши, он больше не может гарантировать, что вы всегда получаете один и тот же объект, если вы читаете одну и ту же запись в базе данных несколько раз в рамках одной сессии.

Вы можете увидеть это в следующем тестовом примере, в котором я выполняю один и тот же запрос дважды.

final StatelessSession statelessSession = sessionFactory.openStatelessSession();

statelessSession.getTransaction().begin();

final Post post1 = statelessSession.createQuery("""
                SELECT p
                FROM Post p
                    JOIN FETCH p.comments
                WHERE p.id=:id""", Post.class)
        .setParameter("id", 1L)
        .getSingleResult();

final Post post2 = statelessSession.createQuery("""
                SELECT p
                FROM Post p
                    JOIN FETCH p.comments
                WHERE p.id=:id""", Post.class)
        .setParameter("id", 1L)
        .getSingleResult();

System.out.println();

statelessSession.getTransaction().commit();

Без кэша первого уровня StatelessSession не знает о ранее загруженных объектах, поэтому он будет создавать новый объект для каждой записи, возвращаемой запросом. Из-за этого вы можете получить несколько объектов, представляющих одну и ту же запись базы данных.

Мы получили два объекта одной записи в базе данных, которые сохранились в разные области памяти кучи.

Используя стандартный экземпляр Session, Hibernate выполнит первый запрос, создаст объект возвращаемой сущности и сохранит ее в кэше первого уровня. После этого он выполнит второй запрос, проверит кэш первого уровня на наличие объекта сущности, и вернет этот объект. Таким образом в рамках одной сессии вы всегда получаете один и тот же объект сущности.

Чтобы нагляднее это продемонстрировать, давайте выполним те же запросы с использованием Session.

Мы получили 2 объекта, которые указывают на одну и ту же область памяти в куче

Пожалуйста, помните об этом при написании бизнес-логики и убедитесь, что вы всегда используете один и тот же объект сущности для операций записи. В противном случае вы можете перезаписать ранее выполненные изменения.

Резюмирую

Интерфейс StatelessSession в Hibernate предоставляет командно-ориентированный API, который дает вам больше контроля над выполняемыми SQL-запросами. Он гораздо ближе к JDBC, но не поддерживает такие функции как сравнение состояния объектов (dirty checks), выталкивание контекста хранения (flushes), кэши всех уровней, каскадные операции и ленивую загрузку.

В целом, StatelessSession в Hibernate - это отличная функция, если вы хотите снизить накладные расходы на обработку сессий в Hibernate и не нуждаетесь во всех этих функциях. Типичными примерами являются пакетные задания или другие сценарии использования, которые выполняют множество простых операций записи.

Struchkov Mark
Struchkov Mark
Задавайте вопросы, если что-то осталось не понятным👇