Миграция на новые версии фреймворков всегда сопровождается сложностями, особенно если в них произошли значительные изменения. В этой статье мы рассмотрим, с какими проблемами я столкнулся при переходе со 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 и базой данных. Важно заранее подготовиться к этим изменениям, провести тестирование и внести корректировки в конфигурацию и код приложения. Надеемся, что мой опыт поможет вам избежать трудностей при обновлении своих приложений.
igorhak
Отдельно хотел отметить, что в некоторых случаях возвращаемые сущности теперь не объекты созданные рефлексией, а proxy объекты.
Поймали недавно баг при переходе между 3.х.х версиями. У нас на проекте используется ACL при доступе к сущностям, в БД хранится тип сущности и id, так вот, MyСlass::class.qualifiedName теперь возвращает имя proxy объекта, а не имя сущности. Пришлось это подлечить.
sergey-gunslinger
Так ведь прокси-объекты у нас создавались и раньше, не? Например, если в объекте использовалась ленивая загрузка сущностей (кстати оттуда же старая песня, что getter’ы сущностей не могут быть final — та же проблема, что и с CDI объектами, требующими специфичного функционала — например, нельзя из final методов юзать инжектируемые поля класса)
igorhak
Я по этому и написал в отдельных случаях. Этот баг всплыл после перехода на 3.3.1.
Объект возвращался из lazy-поля, до перехода все работало.