Transition from Spring Boot 2 to Spring Boot 3 in the context of Hibernate, or how not to go crazy when migrating an entity in a monolith

Migration to new versions of frameworks is always accompanied by difficulties, especially if significant changes have occurred in them. In this article, we'll look at the problems I encountered when migrating from Spring Boot 2.xx to Spring Boot 3.3.1 and Hibernate 6.4+, and provide solutions that may help other developers avoid similar difficulties when upgrading their applications.

It is worth noting that not all problems when migrating monoliths can be solved using automatic migration tools, such as OpenRewriteespecially if you have many entities interconnected by legacy code and complex business logic.

With the release of Spring Boot 3.3.1, developers were faced with the need to migrate to newer versions of Hibernate (6.2+). This update is due to changes to Hibernate that ensure compatibility with new versions of Spring Boot and the use of new features of the framework.

Problems with @OneToOne annotation

One of the first problems I encountered when migrating to Hibernate 6.2+ was changes in the handling of one-to-one associations (@OneToOne).

In Hibernate 6.2+, optional associations marked @OneToOnenow automatically creates a unique constraint in the database. This means that foreign keys for such relationships must be unique. In earlier versions of Hibernate, the unique constraint was not enforced.

If your application is covered by tests, this issue will most likely show up when you run the tests and the build will crash. To solve this problem, you can replace the connection @OneToOne on @ManyToOnewhich prevents the addition of a unique constraint while maintaining application functionality. While this may change the business logic, in most cases it will be the best solution.

Problems with sequence generation

When migrating to Hibernate 6.x, it is important to pay attention to generating unique values ​​for primary keys. Older versions of Hibernate used one global sequence − hibernate_sequencewhich applied to all entities. In Hibernate 6, each entity has its own sequence by default, which can cause bugs in older applications.

Entity Example Product:

@Entity
public class Product {

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

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

Instead of using one global sequence, a separate sequence is now created for each entity, for example product_seq for entity Product.
Possible solutions to the problem:
1) Updating the database schema (creating sequences for each entity)
Example SQL script for creating sequences:

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) Configure Hibernate to use the old sequence (add the following property to your Hibernate config)

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

Problems with parsing varchar arrays in PostgreSQL

When upgrading to a new version of the PostgreSQL driver, an error may occur when working with arrays stored in fields like varchar. The problem may occur when saving empty rows to the database.

In new versions of the PostgreSQL driver, the method org.postgresql.jdbc.arrayDecoding.buildArrayList does not handle empty strings correctly and may throw an exception ArrayIndexOutOfBoundsException(in line 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++;
        }
    }
  //.....

To avoid this error, it is recommended to replace empty rows in the database with null and change the logic for saving data to the database (avoiding saving empty rows).

Changes in JSON mapping on H2

From version H2 1.4.200+ type SqlTypes.JSON now displays as type by default jsonwhereas previously it was used clob. If you encounter errors in your database schema, you may need to perform a conversion using the expression cast(old as json).

Problems with field names in native queries

If you are using native SQL queries with createNativeQueryit is important to consider the uniqueness of column aliases. When using joins and queries like SELECT * this may lead to errors. It is better to explicitly specify the columns to select, as shown in the example:

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

Conclusion

Migrating to Spring Boot 3.3.1 and Hibernate 6.2+ can lead to many problems, especially ORM and database related ones. It is important to prepare for these changes in advance, conduct testing and make adjustments to the configuration and application code. We hope that my experience will help you avoid difficulties when updating your applications.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *