Migration to Hibernate 6

Hibernate 6 was released a while ago and I see more and more teams migrating their persistence levels, or at least getting ready to migrate. As is often the case, the amount of work required to migrate to Hibernate 6 depends on the quality of your code and the version of Hibernate you are currently using.

For most applications using Hibernate 5, migration will be relatively quick and easy. But you will have to fix and update some things if you are still using an older version of Hibernate or some features deprecated in Hibernate 5.

In this article, I’ll walk you through the most important steps to prepare your application for migration and what you need to do when migrating your application.

Prepare persistence layer for Hibernate 6

Not all changes made in Hibernate 6 are backward compatible. Fortunately, most of them can be dealt with before the migration. This will allow you to implement the necessary changes step by step while continuing to use Hibernate 5. This way you will avoid breaking your application and be able to prepare the migration over several releases or sprints.

Upgrade to JPA 3

One example of such a change is the transition to JPA 3. This version of the JPA specification did not bring any new features. But for legal reasons, all package and configuration parameter names have been renamed from javax.persistence.* in jakarta.persistence.*.

Among other things, this change affects the import statements for all mapping annotations and EntityManager and breaks all layers of persistence. The easiest way to fix this is to use the find and replace feature in your IDE. Replacing all occurrences javax.persistence on the jakarta persistence should fix compiler errors and update your configuration.

Hibernate 6 uses JPA 3 by default and you can run a find and replace command as part of the migration. But I recommend changing your project dependency from hibernate-core on the hibernate-core-jakarta and make this change while you are still using Hibernate 5.

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

Replace Criteria API in Hibernate

Another important step in preparing the persistence layer for Hibernate 6 is the replacement of the Criteria API in Hibernate. This API has been deprecated since the first release of Hibernate 5 and you may have already replaced it. But I know that for many applications this is not the case.

You can easily check if you are still using Hibernate’s proprietary Criteria API by checking the deprecation warnings. If you find a deprecation warning telling you that the method createCriteria(Class) is deprecated, then you are still using the old Hibernate API and should replace it. Unfortunately, you can no longer delay this change. Hibernate 6 no longer supports the old, proprietary Criteria API.

The Criteria APIs in JPA and Hibernate are similar. They allow you to dynamically create a query at runtime. Most developers use it to create a query based on user input or the result of some business rule. But even though both APIs have the same name and purpose, there is no easy migration path.

The only way out is to remove the Hibernate API Criteria from the Hibernate persistence layer. You need to override your queries using JPA’s Criteria API. Depending on the number of queries you need to replace and their complexity, this may take some time. Hibernate 5 supports both Criteria APIs and I recommend that you replace the old queries one by one before moving to Hibernate 6.

Each request is different and requires different steps to migrate. This makes it difficult to estimate how long such a replacement will take and how to carry it out. But some time ago I wrote managementexplaining how to port the most commonly used query features from Hibernate to the JPA Criteria API.

Define the SELECT clause for your queries

For all query operators that you may statically define when you implement your application, you most likely use JPQL or a Hibernate-specific HQL extension.

When using HQL, Hibernate can generate the SELECT clause of your query based on the FROM clause. In this case, your query selects all entity classes specified in the FROM clause. Unfortunately this has changed in Hibernate 6 for all queries that combine multiple entity classes.

In Hibernate 5, a query that joins multiple entity classes returns Object[] or ListA that contains all the entities combined in the FROM clause.

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

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

So for the query statement in the previous code snippet, Hibernate generated a SELECT clause that referenced entities Author and Book. The generated statement was identical to the following.

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

For the same HQL statement, Hibernate 6 only generates a SELECT clause that selects the root object of your FROM clause. In this example, it selects only the object Authorbut not an object Book.

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

This change does not cause any compiler errors, but it does create problems in the code that processes the query result. At best, you have a few test cases that will detect these errors.

But I recommend adding a SELECT clause that references the Author and Book entities while you are still using Hibernate 5. This will not change anything for Hibernate 5, but it will ensure that you get the same query result when using Hibernate 6 as when using Hibernate 5.

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

Migration to Hibernate 6

After implementing the changes described in the previous section, your migration to Hibernate 6 should be straightforward and require only a few configuration changes.

Default sequence names

Generating unique primary key values ​​is the first thing you should check after migrating your persistence layer to Hibernate 6. You need to make a small change if you are using database sequences and do not specify a sequence for each entity.

Here is an example entity Authorin which only sequence generation strategy, but it doesn’t specify what sequence Hibernate should use. In such situations, Hibernate uses the default sequence.

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

In versions 4 and 5, Hibernate used one default sequence for the entire persistence unit. She was called 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
        (?, ?, ?, ?)

As I showed in recent article, Hibernate 6 has changed this approach. By default, it uses a separate sequence for each entity class. The name of this sequence consists of the name of the entity and the postfix _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

This approach is good, and many developers will feel more comfortable with it. But it breaks existing applications because entity-specific sequences don’t exist in the database.

You have two options for solving this problem:

  1. Update database schemato add new sequences.

  2. Add a config option to tell Hibernate to use the old default sequences.

When working on a migration, I recommend using the 2nd approach. This is the fastest and easiest way to fix the problem, and you can still add new sequences in a future release.

You can tell Hibernate to use the old default sequences by setting the property hibernate.id.db_structure_naming_strategy in your persistence.xml. By setting this value to single, you will get the default sequences used by Hibernate < 5.3. And the configuration value legacy allows you to get the default sequence names used by 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>

I have explained all this in more detail in my guide to sequence naming strategies used in Hibernate 6.

Instant and Duration Displays

Another change that can be easily overlooked until the deployment of the migrated persistence layer fails is Instant and Duration display.

When Hibernate introduced proprietary mapping for these types in version 5, it rendered instant on the SqlType.TIMESTAMP and duration on the Types.BIGINT. Moving to Hibernate 6 changes this mapping. Now it displays instant on the SqlType.TIMESTAMP_UTC and duration on the SqlType.INTERVAL_SECOND.

These new mappings seem to be more appropriate than the old ones. So it’s good that they changed them in Hibernate 6. But it still breaks table mapping in existing applications. If you run into this problem, you can set the config property hibernate.type.preferred_instant_jdbc_type in TIMESTAMP and hibernate.type.preferred_duration_jdbc_type in 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>

These are two new configuration options introduced in Hibernate 6. They are both marked as incubation. This means that they may change in the future. Therefore, I recommend that you use them during your transition to Hibernate 6, and adjust your table model to match the new default Hibernate mapping shortly thereafter.

New logging categories

If you’ve read some of my previous articles on Hibernate 6, you should know that the Hibernate team has rewritten the code that generates query statements. One of the side effects of this change was a slight change in logging configuration.

In Hibernate 5 you need to enable trace logging for category org.hibernate.type.descriptor.sqlto log all binding parameter values ​​and values ​​retrieved from the result set.

<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 introduced a separate logging category for bind parameter values. You can enable logging of these values ​​by configuring trace logging for the category 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%]

Conclusion

Several changes have been made in Hibernate 6 that break backwards compatibility. Not all of them require huge changes to your persistence layer code. It may be enough to add just a few configuration options to keep the old behavior.

But two changes will require special attention. This is an upgrade to JPA 3 and removal of the deprecated Criteria API from Hibernate. I recommend that you deal with both changes while you are still using Hibernate 5.

Upgrading to JPA 3 requires you to change the configuration parameter names and import statements of all classes, interfaces, and annotations defined by the specification. But don’t worry. It usually sounds worse than it actually is. I have migrated several projects by doing a simple find and replace operation in my IDE. This was usually done within a few minutes.

Removing the deprecated Criteria API in Hibernate will cause more serious problems. You will need to rewrite all requests that use the old API. I recommend that you do this while you are still using Hibernate 5. It still supports the old Hibernate Criteria API and the JPA Criteria API. This way you can replace one request after another without breaking your application.

Similar Posts

Leave a Reply