Миграция на новые версии фреймворков всегда сопровождается сложностями, особенно если в них произошли значительные изменения. В этой статье мы рассмотрим, с какими проблемами я столкнулся при переходе со Spring Boot 2.x.x на Spring Boot 3.3.1 и Hibernate 6.4+, а также предложим решения, которые могут помочь другим разработчикам избежать аналогичных трудностей при обновлении своих приложений.

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

С выходом Spring Boot 3.3.1 разработчики столкнулись с необходимостью перехода на более новые версии Hibernate (6.2+). Это обновление связано с изменениями в Hibernate, обеспечивающими совместимость с новыми версиями Spring Boot и использование новых возможностей фреймворка.

Проблемы с аннотацией @OneToOne

Одной из первых проблем, с которыми я столкнулся при переходе на Hibernate 6.2+, были изменения в обработке ассоциаций один-к-одному (@OneToOne).

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

Если ваше приложение покрыто тестами, эта проблема, скорее всего, обнаружится при запуске тестов, и сборка упадет. Для решения этой проблемы можно заменить связь @OneToOne на @ManyToOne, что предотвращает добавление уникального ограничения, сохраняя функциональность приложения. Хотя это может изменить бизнес-логику, в большинстве случаев это будет лучшим решением.

Проблемы с генерацией последовательностей

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

Пример сущности Product:

@Entity
public class Product {

    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE)
    private Long id;

    // другие поля и методы
} 

Вместо использования одной глобальной последовательности теперь для каждой сущности создается отдельная последовательность, например product_seq для сущности Product.
Возможные решения проблемы :
1) Обновление схемы базы данных (создание последовательностей для каждой сущности)
Пример SQL-скрипта для создания последовательностей:

sql
Копировать код
DO $$
BEGIN
    IF NOT EXISTS (SELECT 1 FROM pg_class WHERE relkind = 'S' AND relname = 'product_seq') THEN
        CREATE SEQUENCE product_seq;
    END IF;
END $$;

DO $$
DECLARE
    max_id bigint;
BEGIN
    SELECT COALESCE(MAX(id), 1) INTO max_id FROM product;
    PERFORM setval('product_seq', max_id);
END $$;

2) Настройка Hibernate на использование старой последовательности (добавьте в конфигурацию Hibernate следующее свойство)

<property name="hibernate.id.db_structure_naming_strategy" value="legacy"/>

Проблемы с парсингом массивов из varchar в PostgreSQL

При переходе на новую версию драйвера PostgreSQL может возникнуть ошибка при работе с массивами, сохраненными в полях типа varchar. Проблема может возникнуть при сохранении пустых строк в базу данных.

В новых версиях драйвера PostgreSQL метод org.postgresql.jdbc.arrayDecoding.buildArrayList некорректно обрабатывает пустые строки и может вызывать исключение ArrayIndexOutOfBoundsException(в 16 строке).

static PgArrayList buildArrayList(String fieldString, char delim) {
    final PgArrayList arrayList = new PgArrayList();
    if (fieldString == null) {
        return arrayList;
    }

    final char[] chars = fieldString.toCharArray();
    StringBuilder buffer = null;
    boolean insideString = false;
    boolean wasInsideString = false;
    final List<PgArrayList> dims = new ArrayList<>();
    PgArrayList curArray = arrayList;

    int startOffset = 0;
    {
        if (chars[0] == '[') {
            while (chars[startOffset] != '=') {
                startOffset++;
            }
            startOffset++;
        }
    }
  //.....


Чтобы избежать этой ошибки, рекомендуется заменить пустые строки в базе данных на null , и изменить логику сохранения данных в базу (избегая сохранения пустых строк).

Изменения в маппинге JSON на H2

С версии H2 1.4.200+ тип SqlTypes.JSON теперь по умолчанию отображается как тип json, тогда как раньше использовался clob. Если вы столкнулись с ошибками в схеме базы данных, вам может потребоваться выполнение преобразования с помощью выражения cast(old as json).

Проблемы с именами полей в собственных запросах

Если вы используете собственные SQL-запросы с createNativeQuery, важно учитывать уникальность псевдонимов столбцов. При использовании объединений (join) и запросов типа SELECT * это может привести к ошибкам. Лучше явно указывать столбцы для выборки, как показано в примере:

session.createNativeQuery(
    "SELECT p.* FROM person p LEFT JOIN dog d on d.person_id = p.id", Person.class
).getResultList();

Заключение

Миграция на Spring Boot 3.3.1 и Hibernate 6.2+ может привести к множеству проблем, особенно связанных с ORM и базой данных. Важно заранее подготовиться к этим изменениям, провести тестирование и внести корректировки в конфигурацию и код приложения. Надеемся, что мой опыт поможет вам избежать трудностей при обновлении своих приложений.

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


  1. igorhak
    16.10.2024 08:21

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

    Поймали недавно баг при переходе между 3.х.х версиями. У нас на проекте используется ACL при доступе к сущностям, в БД хранится тип сущности и id, так вот, MyСlass::class.qualifiedName теперь возвращает имя proxy объекта, а не имя сущности. Пришлось это подлечить.


    1. sergey-gunslinger
      16.10.2024 08:21

      Так ведь прокси-объекты у нас создавались и раньше, не? Например, если в объекте использовалась ленивая загрузка сущностей (кстати оттуда же старая песня, что getter’ы сущностей не могут быть final — та же проблема, что и с CDI объектами, требующими специфичного функционала — например, нельзя из final методов юзать инжектируемые поля класса)


      1. igorhak
        16.10.2024 08:21

        Я по этому и написал в отдельных случаях. Этот баг всплыл после перехода на 3.3.1.

        Объект возвращался из lazy-поля, до перехода все работало.