Hibernate 6 has been released for a while, and I see more and more teams migrating their persistence layers or at least preparing for the migration. As so often, the work required for the migration to Hibernate 6 depends on your code quality and the Hibernate version you’re currently using.

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

In this article, I will show you the most important steps to prepare your application for migration and what you need to do when migrating your application.

If you want to get into more details, see an example migration, and learn what you should do after the migration, you should join the Persistence Hub. I showed all of that in great detail in a recent Expert Session, and Steve Ebersole (Hibernate 6 Lead Developer) did a deep dive into Hibernate 6 in a previous Expert Session. As a member, you can find the recording of both sessions in the archives.

Prepare your persistence layer for Hibernate 6

Not all of the changes introduced in Hibernate 6 are backward compatible. Luckily, you can handle most of them before you perform the migration. That enables you to implement the required changes step by step while still using Hibernate 5. So you will avoid breaking your application, and you can prepare the migration over multiple releases or sprints.

Update to JPA 3

One example of such a change is the migration to JPA 3. That version of the JPA specification didn’t bring any new features. But for legal reasons, all package and configuration parameter names got renamed from javax.persistence.* to jakarta.persistence.*.

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

Hibernate 6 uses JPA 3 by default, and you could run the search and replace command as part of your migration. But I recommend changing your project’s dependency from hibernate-core to hibernate-core-jakarta and performing this change while you’re still using Hibernate 5.

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

Replace Hibernate’s Criteria API

Another important step to prepare your persistence layer for Hibernate 6 is to replace Hibernate’s Criteria API. That API has been deprecated since the first release of Hibernate 5, and you might have already replaced it. But I know that that’s not the case for many applications.

You can easily check if you’re still using Hibernate’s proprietary Criteria API by checking your deprecation warnings. If you find any deprecation warning telling you that the method createCriteria(Class) is deprecated, you’re still using Hibernate’s old API and need to replace it. Unfortunately, you can no longer postpone that change. Hibernate 6 no longer supports the old, proprietary Criteria API.

JPA’s and Hibernate’s Criteria API are similar. They enable you to build a query dynamically at runtime. Most developers use that to create a query based on user input or the result of some business rules. But even though both APIs share the same name and goals, there is no easy migration path.

Your only option is to remove Hibernate’s Criteria API from your persistence layer. You need to reimplement your queries using JPA’s Criteria API. Depending on the number of queries you need to replace and their complexity, this might take a while. Hibernate 5 supports both Criteria APIs, and I recommend you replace the old queries one by one before you upgrade to Hibernate 6.

Every query is different and requires different steps to migrate it. That makes it difficult to estimate how long such a replacement will take and how to do it. But a while ago, I wrote a guide explaining how to migrate the most commonly used query features from Hibernate’s to JPA’s Criteria API.

Define SELECT clauses for your queries

For all the query statements you can statically define while implementing your application, you’re most likely using JPQL or the Hibernate-specific extension called HQL.

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

In Hibernate 5, a query that joins multiple entity classes returns an Object[] or a List<Object[]> containing all entities joined in the FROM clause.

// query with implicit SELECT clause
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 the Author and the Book entity. The generated statement was identical to the following one.

--generated query using 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 would only select the Author entity but not the Book entity.

--generated query using Hibernate 6
SELECT a FROM Author a JOIN a.books

This change doesn’t cause any compiler errors but causes problems in the code that processes the query result. In the best cases, you have some test cases that will find these bugs.

But I recommend adding a SELECT clause that references the Author and Book entity while you’re still using Hibernate 5. This will not change anything for Hibernate 5, but it ensures that you get the same query result using Hibernate 6 as you got using Hibernate 5.

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

Migrating to Hibernate 6

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

Default sequence names

The generation of unique primary key values is the 1st thing you should check after you migrate your persistence layer to Hibernate 6. You need to apply a small change if you use database sequences and don’t specify a sequence for every entity.

Here you can see an example of an Author entity that only sets the generation strategy to sequence but doesn’t specify which sequence Hibernate shall use. In these situations, Hibernate uses a default sequence.

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

In versions 4 and 5, Hibernate used 1 default sequence for the entire persistence unit. It 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 a recent article, Hibernate 6 changed this approach. By default, it uses a different sequence for every entity class. The name of that sequence consists of the entity’s name 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 fine, and many developers will feel more comfortable with it. But it breaks existing applications because the entity-specific sequences don’t exist in the database.

You have 2 options to solve this problem:

  1. Update your database schema to add the new sequences.
  2. Add a configuration parameter to tell Hibernate to use the old default sequences.

When working on the migration, I recommend using the 2nd approach. It’s the quickest and easiest way to solve the problem, and you can still add the new sequences in a future release.

You can tell Hibernate to use the old default sequences by configuring the property hibernate.id.db_structure_naming_strategy in your persistence.xml. Setting this value to single gets you the default sequences used by Hibernate <5.3. And the configuration value legacy gets you 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 explained all of this in more detail in my guide to the sequence naming strategies used by Hibernate 6.

Instant and Duration mappings

Another change that you can easily overlook until the deployment of your migrated persistence layer fails is the mapping of Instant and Duration.

When Hibernate introduced the proprietary mapping for these types in version 5, it mapped Instant to SqlType.TIMESTAMP and Duration to Types.BIGINT. Hibernate 6 changes this mapping. It now maps Instant to SqlType.TIMESTAMP_UTC and Duration to SqlType.INTERVAL_SECOND.

These new mappings seem to be a better fit than the old ones. So, it’s good that they changed it in Hibernate 6. But it still breaks the table mapping of existing applications. If you run into that problem, you can set the configuration property hibernate.type.preferred_instant_jdbc_type to TIMESTAMP and hibernate.type.preferred_duration_jdbc_type to 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 2 new configuration parameters introduced in Hibernate 6. Both are marked as incubating. This means that they might change in the future. I, therefore, recommend you use them during your migration to Hibernate 6 and adjust your table model soon after so that it matches Hibernate’s new standard mapping.

New logging categories

If you read some of my previous articles about Hibernate 6, you should know that the Hibernate team rewrote the code that generates the query statements. One of the side effects of that change was a small change in the logging configuration.

In Hibernate 5, you need to activate trace logging for the category org.hibernate.type.descriptor.sql to log all bind parameter values and the values extracted 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 the bind parameter values. You can activate the logging of those values by configuring trace logging for the org.hibernate.orm.jdbc.bind category.

<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

Hibernate 6 introduced a few changes that break backward compatibility. Not all of them require huge changes to the code of your persistence layer. It might be enough to only add a few configuration parameters to keep the old behavior.

But 2 changes will require special attention. These are the update to JPA 3 and the removal of Hibernate’s deprecated Criteria API. I recommend you handle both while you’re still using Hibernate 5.

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

The removal of Hibernate’s deprecated Criteria API will cause bigger issues. You will need to rewrite all queries that use the old API. I recommend you do that while you’re still using Hibernate 5. It still supports Hibernate’s old Criteria API and JPA’s Criteria API. So, you can replace one query after the other without breaking your application.