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

Для большинства приложений, использующих Hibernate 5, миграция будет относительно быстрой и простой. Но вам придется исправить и обновить некоторые вещи, если вы все еще используете более старую версию Hibernate или некоторые функции, устаревшие в Hibernate 5.

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

Подготовьте уровень персистентности для Hibernate 6

Не все изменения, внесенные в Hibernate 6, являются обратно совместимыми. К счастью, с большинством из них можно разбираться до выполнения миграции. Это позволит вам внедрить необходимые изменения шаг за шагом, продолжая использовать Hibernate 5. Таким образом, вы избежите поломки вашего приложения и сможете подготовить миграцию в течение нескольких релизов или спринтов.

Обновите до JPA 3

Одним из примеров такого изменения является переход на JPA 3. Эта версия спецификации JPA не принесла никаких новых функций. Но по юридическим причинам все имена пакетов и конфигурационных параметров были переименованы из javax.persistence.* в jakarta.persistence.*.

Кроме всего прочего, это изменение влияет на инструкции импорта для всех аннотаций отображения (mapping) и EntityManager и ломает все слои персистентности. Самый простой способ исправить это - использовать функцию поиска и замены в вашей IDE. Замена всех вхождений javax.persistence на jakarta.persistence должна исправить ошибки компилятора и обновить вашу конфигурацию.

Hibernate 6 по умолчанию использует JPA 3, и вы можете выполнить команду поиска и замены как часть миграции. Но я рекомендую изменить зависимость вашего проекта с hibernate-core на hibernate-core-jakarta и выполнить это изменение, пока вы все еще используете Hibernate 5.

<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-core-jakarta</artifactId>
    <version>5.6.12.Final</version>
</dependency>

Замените API Criteria в Hibernate

Еще одним важным шагом для подготовки уровня персистентности к Hibernate 6 является замена API Criteria в Hibernate. Этот API был устаревшим с момента первого выпуска Hibernate 5, и вы, возможно, уже заменили его. Но я знаю, что для многих приложений это не так.

Вы можете легко проверить, используете ли вы по-прежнему проприетарный API Criteria от Hibernate, проверив предупреждения об устаревании. Если вы найдете предупреждение об устаревании, сообщающее вам, что метод createCriteria(Class) устарел, значит, вы все еще используете старый API Hibernate и должны его заменить. К сожалению, вы больше не можете откладывать это изменение. Hibernate 6 больше не поддерживает старый, проприетарный API Criteria.

API Criteria в JPA и Hibernate аналогичны. Они позволяют вам динамически создавать запрос во время выполнения. Большинство разработчиков используют его для создания запроса на основе пользовательского ввода или результата некоторых бизнес-правил. Но даже несмотря на то, что оба API имеют одинаковое название и цели, простого пути миграции не существует.

Единственный выход - удалить Hibernate API Criteria из уровня персистентности Hibernate. Вам необходимо переопределить свои запросы, используя API Criteria от JPA. В зависимости от количества запросов, которые вам нужно заменить, и их сложности, это может занять некоторое время. Hibernate 5 поддерживает оба API Criteria, и я рекомендую вам заменить старые запросы один за другим до перехода на Hibernate 6.

Каждый запрос индивидуален и требует разных шагов для переноса. Это затрудняет оценку того, сколько времени займет такая замена и как ее выполнить. Но некоторое время назад я написал руководство, объясняющее, как перенести наиболее часто используемые функции запросов из Hibernate в JPA Criteria API.

Определите предложение SELECT для ваших запросов

Для всех операторов запросов, которые вы можете статически определить при реализации приложения, вы, скорее всего, используете JPQL или специфическое для Hibernate расширение HQL.

При использовании HQL Hibernate может сгенерировать предложение SELECT вашего запроса на основе предложения FROM. В этом случае ваш запрос выбирает все классы сущностей, указанные в предложении FROM. К сожалению, это изменилось в Hibernate 6 для всех запросов, которые объединяют несколько классов сущностей.

В Hibernate 5 запрос, объединяющий несколько классов сущностей, возвращает Object[] или List<Object[]>, содержащий все сущности, объединенные в предложении FROM.

// запрос с неявным предложением SELECT

List<Object[]> results = em.createQuery("FROM Author a JOIN a.books b").getResultList();

Таким образом, для оператора запроса в предыдущем фрагменте кода Hibernate сгенерировала предложение SELECT, которое ссылалось на сущности Author и Book. Сгенерированный оператор был идентичен следующему.

--запрос, сформированный с использованием Hibernate 5
SELECT a, b FROM Author a JOIN a.books b

Для того же оператора HQL Hibernate 6 генерирует только предложение SELECT, которое выбирает корневой объект вашего предложения FROM. В данном примере он выбирает только объект Author, но не объект Book.

--запрос, сформированный с использованием Hibernate 6
SELECT a FROM Author a JOIN a.books

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

Но я рекомендую добавить предложение SELECT, которое ссылается на сущность Author и Book, пока вы еще используете Hibernate 5. Это ничего не изменит для Hibernate 5, но гарантирует, что вы получите тот же результат запроса при использовании Hibernate 6, что и при использовании Hibernate 5.

// определиение предложения SELECT
List<Object[]> results = em.createQuery("SELECT a, b FROM Author a JOIN a.books b").getResultList();

Миграция на Hibernate 6

После реализации изменений, описанных в предыдущем разделе, ваша миграция в Hibernate 6 должна быть простой и потребовать лишь нескольких изменений конфигурации.

Имена последовательностей по умолчанию

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

Здесь показан пример сущности Author, в которой задана только стратегия генерации последовательности, но не указано, какую последовательность должен использовать Hibernate. В таких ситуациях Hibernate использует последовательность по умолчанию.

@Entity
public class Author {
     
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE)
    private Long id;
     
    ...
}

В версиях 4 и 5 Hibernate использовала одну последовательность по умолчанию для всей единицы персистентности. Она называлась hibernate_sequence.

08:18:36,724 DEBUG [org.hibernate.SQL] - 
    select
        nextval('hibernate_sequence')
08:18:36,768 DEBUG [org.hibernate.SQL] - 
    insert
    into
        Author
        (firstName, lastName, version, id) 
    values
        (?, ?, ?, ?)

Как я показал в недавней статье, Hibernate 6 изменила этот подход. По умолчанию он использует отдельную последовательность для каждого класса сущностей. Имя этой последовательности состоит из имени сущности и постфикса _SEQ.

08:24:21,772 DEBUG [org.hibernate.SQL] - 
    select
        nextval('Author_SEQ')
08:24:21,778 WARN  [org.hibernate.engine.jdbc.spi.SqlExceptionHelper] - SQL Error: 0, SQLState: 42P01
08:24:21,779 ERROR [org.hibernate.engine.jdbc.spi.SqlExceptionHelper] - ERROR: relation "author_seq" does not exist
  Position: 16

Такой подход хорош, и многие разработчики будут чувствовать себя с ним более комфортно. Но он нарушает работу существующих приложений, потому что последовательности, специфические для сущности не существуют в базе данных.

У вас есть два варианта решения этой проблемы:

  1. Обновить схему базы данных, чтобы добавить новые последовательности.

  2. Добавить параметр конфигурации, чтобы указать Hibernate на использование старых последовательностей по умолчанию.

При работе над миграцией я рекомендую использовать 2-й подход. Это самый быстрый и простой способ решить проблему, и вы все равно сможете добавить новые последовательности в будущем релизе.

Вы можете указать Hibernate использовать старые последовательности по умолчанию, настроив свойство hibernate.id.db_structure_naming_strategy в вашем persistence.xml. Установив это значение в single, вы получите последовательности по умолчанию, используемые Hibernate <5.3. А конфигурационное значение legacy позволяет получить имена последовательностей по умолчанию, используемые Hibernate >=5.3.

<persistence>
    <persistence-unit name="my-persistence-unit">
        ...
        <properties>
            <! – ensure backward compatibility – >
            <property name="hibernate.id.db_structure_naming_strategy" value="legacy" />
 
            ...
        </properties>
    </persistence-unit>
</persistence>

Я объяснил все это более подробно в своем руководстве по стратегиям именования последовательностей, используемым в Hibernate 6.

Отображения Instant и Duration

Еще одно изменение, которое можно легко не заметит до тех пор, пока развертывание перенесенного слоя персистентности не даст сбой, - это отображение Instant и Duration.

Когда Hibernate ввела проприетарное отображение для этих типов в версии 5, он отображал Instant на SqlType.TIMESTAMP и Duration на Types.BIGINT. Переход на Hibernate 6 изменяет это отображение. Теперь он отображает Instant на SqlType.TIMESTAMP_UTC и Duration на SqlType.INTERVAL_SECOND.

Эти новые отображения кажутся более подходящими, чем старые. Поэтому хорошо, что они изменили их в Hibernate 6. Но это по-прежнему нарушает отображение таблиц в существующих приложениях. Если вы столкнетесь с этой проблемой, вы можете установить свойство конфигурации hibernate.type.preferred_instant_jdbc_type в TIMESTAMP и hibernate.type.preferred_duration_jdbc_type в BIGINT.

<persistence>
    <persistence-unit name="my-persistence-unit">
        <properties>
            <! – ensure backward compatibility – >
            <property name="hibernate.type.preferred_duration_jdbc_type" value="BIGINT" />
            <property name="hibernate.type.preferred_instant_jdbc_type" value="TIMESTAMP" />
 
            ...
        </properties>
    </persistence-unit>
</persistence>

Это два новых параметра конфигурации, введенные в Hibernate 6. Оба они помечены как инкубационные. Это означает, что они могут измениться в будущем. Поэтому я рекомендую вам использовать их во время перехода на Hibernate 6 и вскоре после этого скорректировать модель таблицы так, чтобы она соответствовала новому стандартному отображению Hibernate.

Новые категории ведения журнала

Если вы читали некоторые из моих предыдущих статей о Hibernate 6, вы должны знать, что команда Hibernate переписала код, генерирующий операторы запросов. Одним из побочных эффектов этого изменения стало небольшое изменение в конфигурации ведения журнала.

В Hibernate 5 необходимо активировать ведение журнала трассировки для категории org.hibernate.type.descriptor.sql, чтобы регистрировать все значения параметров привязки и значения, извлеченные из результирующего набора.

<Configuration>
  ...
  <Loggers>
    <Logger name="org.hibernate.SQL" level="debug"/>
    <Logger name="org.hibernate.type.descriptor.sql" level="trace"/>
    ...
  </Loggers>
</Configuration>
19:49:20,330 DEBUG [org.hibernate.SQL] - 
    select
        this_.id as id1_0_1_,
        this_.firstName as firstnam2_0_1_,
        this_.lastName as lastname3_0_1_,
        this_.version as version4_0_1_,
        books3_.authors_id as authors_2_2_,
        book1_.id as books_id1_2_,
        book1_.id as id1_1_0_,
        book1_.publisher_id as publishe4_1_0_,
        book1_.title as title2_1_0_,
        book1_.version as version3_1_0_ 
    from
        Author this_ 
    inner join
        Book_Author books3_ 
            on this_.id=books3_.authors_id 
    inner join
        Book book1_ 
            on books3_.books_id=book1_.id 
    where
        book1_.title like ?
19:49:20,342 TRACE [org.hibernate.type.descriptor.sql.BasicBinder] - binding parameter [1] as [VARCHAR] - [%Hibernate%]
19:49:20,355 TRACE [org.hibernate.type.descriptor.sql.BasicExtractor] - extracted value ([id1_1_0_] : [BIGINT]) - [1]
19:49:20,355 TRACE [org.hibernate.type.descriptor.sql.BasicExtractor] - extracted value ([id1_0_1_] : [BIGINT]) - [1]
19:49:20,359 TRACE [org.hibernate.type.descriptor.sql.BasicExtractor] - extracted value ([publishe4_1_0_] : [BIGINT]) - [1]
19:49:20,359 TRACE [org.hibernate.type.descriptor.sql.BasicExtractor] - extracted value ([title2_1_0_] : [VARCHAR]) - [Hibernate]
19:49:20,360 TRACE [org.hibernate.type.descriptor.sql.BasicExtractor] - extracted value ([version3_1_0_] : [INTEGER]) - [0]
19:49:20,361 TRACE [org.hibernate.type.descriptor.sql.BasicExtractor] - extracted value ([firstnam2_0_1_] : [VARCHAR]) - [Max]
19:49:20,361 TRACE [org.hibernate.type.descriptor.sql.BasicExtractor] - extracted value ([lastname3_0_1_] : [VARCHAR]) - [WroteABook]
19:49:20,361 TRACE [org.hibernate.type.descriptor.sql.BasicExtractor] - extracted value ([version4_0_1_] : [INTEGER]) - [0]
19:49:20,361 TRACE [org.hibernate.type.descriptor.sql.BasicExtractor] - extracted value ([id1_1_0_] : [BIGINT]) - [1]
19:49:20,362 TRACE [org.hibernate.type.descriptor.sql.BasicExtractor] - extracted value ([id1_0_1_] : [BIGINT]) - [3]
19:49:20,362 TRACE [org.hibernate.type.descriptor.sql.BasicExtractor] - extracted value ([firstnam2_0_1_] : [VARCHAR]) - [Paul]
19:49:20,362 TRACE [org.hibernate.type.descriptor.sql.BasicExtractor] - extracted value ([lastname3_0_1_] : [VARCHAR]) - [WritesALot]
19:49:20,362 TRACE [org.hibernate.type.descriptor.sql.BasicExtractor] - extracted value ([version4_0_1_] : [INTEGER]) - [0]

В Hibernate 6 введена отдельная категория ведения журнала для значений параметров привязки (bind). Вы можете активировать ведение журнала этих значений, настроив ведение журнала трассировки для категории org.hibernate.orm.jdbc.bind.

<Configuration>
  ...
  <Loggers>
    <Logger name="org.hibernate.SQL" level="debug"/>
    <Logger name="org.hibernate.orm.jdbc.bind" level="trace"/>
    ...
  </Loggers>
</Configuration>
19:52:11,012 DEBUG [org.hibernate.SQL] - 
    select
        a1_0.id,
        a1_0.firstName,
        a1_0.lastName,
        a1_0.version 
    from
        Author a1_0 
    join
        (Book_Author b1_0 
    join
        Book b1_1 
            on b1_1.id=b1_0.books_id) 
                on a1_0.id=b1_0.authors_id 
        where
            b1_1.title like ? escape ''
19:52:11,022 TRACE [org.hibernate.orm.jdbc.bind] - binding parameter [1] as [VARCHAR] - [%Hibernate%]

Заключение

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

Но два изменения потребуют особого внимания. Это обновление до JPA 3 и удаление устаревшего API Criteria из Hibernate. Я рекомендую вам разобраться с обоими изменениями, пока вы все еще используете Hibernate 5.

Обновление до JPA 3 требует, чтобы вы изменили имена параметров конфигурации и инструкции импорта всех классов, интерфейсов и аннотаций, определенных спецификацией. Но не волнуйтесь. Обычно это звучит хуже, чем есть на самом деле. Я перенес несколько проектов, выполнив простую операцию поиска и замены в своей IDE. Обычно это делалось за несколько минут.

Удаление устаревшего API Criteria в Hibernate вызовет более серьезные проблемы. Вам нужно будет переписать все запросы, которые используют старый API. Я рекомендую вам сделать это, пока вы еще используете Hibernate 5. Он по-прежнему поддерживает старый API Criteria от Hibernate и API Criteria от JPA. Таким образом, вы сможете заменять один запрос за другим, не нарушая работу вашего приложения.

Комментарии (3)


  1. Dmitry2019
    24.11.2022 16:21

    Иногда требуется доступ к кэшу Hibernate. Например, чтобы сбросить кэш, когда кто-то руками в базе поигрался, или другое приложение записало что-то в базу (триггер например). Я так и не нашёл, как добраться до кэша в новой версии.

    Также в старой версии есть очень полезная фича скроллирования через результаты. Также не нашёл, как это сделать эффективно без доступа к сессии, хотя unwrap вроде осталось.


  1. val6852 Автор
    24.11.2022 17:49

    Hibernate ORM 6.1.5.Final User Guide

    - раздел 13.7. Managing the cached data:

    https://docs.jboss.org/hibernate/orm/6.1/userguide/html_single/Hibernate_User_Guide.html#caching-management


  1. grossws
    26.11.2022 05:21

    Ну ок, тоже про Java EE -> Jakarta EE xD