|

8 things you need to know when migrating to Hibernate 6.x


Take your skills to the next level!

The Persistence Hub is the place to be for every Java developer. It gives you access to all my premium video courses, monthly Java Persistence News, monthly coding problems, and regular expert sessions.


Hibernate 6 has been released for a while, and the latest Spring Data JPA version includes it as a dependency. So, it’s no surprise that more and more developers want to migrate their applications to the latest Hibernate version. As you’re reading this article, you’re probably one of them. And you might ask yourself what you need to do to migrate your application and what you get out of it. I summarized that in this article and included links to further articles that explain the different features in great detail.

As Steve Ebersole (Lead Developer Hibernate 6) explained when he joined us for an Expert Session in the Persistence Hub, Hibernate 6.0 brought us many internal improvements. And for a moment, it seemed like we wouldn’t get much out of it for our day-to-day projects. But with versions 6.1 and 6.2, we start to see the benefits of those changes. Both releases included a bunch of interesting improvements and new features. So, if you decide to migrate your application, I recommend you skip version 6.0 and directly migrate to the latest version. And if you decide to do that, here are 8 things you should know.

1. Hibernate 6 is based on JPA 3, and that requires changes

With the release of version 6.0.0.Final, the Hibernate team updated the dependency to the Jakarta Persistence API (JPA) specification to version 3.

That’s not an unexpected change, and thanks to the strict compatibility rules, updating the JPA specification usually doesn’t have a huge effect on existing applications. But updating to JPA 3 is different.

As part of the transition of the JPA specification from Oracle to the Eclipse Foundation, the expert group had to change all package and configuration parameter names from javax.persistence.* to jakarta.persistence.*. That means you need to change all import statements and your configuration files.

But don’t worry; it’s not as bad as it might seem. I supported several teams in migrating their applications to Hibernate 6. You can migrate your application from JPA 2 to version 3 in just a few minutes by performing these simple steps:

  • Use the search and replace command in your IDE or on the command line to replace javax.persistence with jakarta.persistence.
  • Build your application and run your test suite to ensure everything works as expected.

As I explained in my migration guide, I recommend doing this before migrating to Hibernate 6 by using a Hibernate 5 version that supports JPA 3.

2. Hibernate’s proprietary Criteria API got removed

Another change in Hibernate 6 that might require changes to your existing code base is the removal of Hibernate’s proprietary Criteria API.

In version 5, Hibernate supported its old deprecated version of the Criteria API and the Criteria API defined by the JPA specification. Both APIs do the same, and it doesn’t make much sense to keep supporting both of them. So, after the proprietary Criteria API had been deprecated for several years, the Hibernate ORM team removed it in version 6.

This is bad news for you if you currently see any deprecation warnings when working with a Criteria API. As I explained in a previous article, there is no easy way to migrate your proprietary Criteria API queries to JPA’s Criteria API. You will need to reimplement each query using the new API.

I have coached several teams during such a migration and explained my process to migrate queries from Hibernate proprietary to JPA’s Criteria API in this article: Migrating from Hibernate’s to JPA’s Criteria API.

3. New naming strategy for default sequences

Another small change you should know about is Hibernate 6’s new default naming strategies for database sequences.

This change affects all entity classes for which you modeled a numerical primary key and told Hibernate to use a database sequence to generate the value without specifying which sequence it shall use.

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

As I explain in the Hibernate Performance Tuning courses included in the Persistence Hub, using a database sequence to generate primary key values provides the best performance. It enables Hibernate to apply several internal optimizations to reduce the number of executed statements and delay their execution as long as possible. This provides significant performance benefits compared to using an autoincremented column.

When using this approach, you can either define which sequence Hibernate shall use or use Hibernate’s default sequence. In previous versions, Hibernate used the sequence hibernate_sequence as the default sequence for all entity classes. Many developers were concerned about this approach because it created huge gaps in their primary key values, and they worried that they might hit the upper limit of their data type.

When people ask me about this, I always explain that you don’t need to worry about any of these concerns. Considering that the maximum value of a Long is 9,223,372,036,854,775,807, it’s rather unlikely that your application will use up all available values. And independent of the number of sequences you’re using, you can’t avoid gaps in your primary key values. If a transaction gets rolled back after you get a value from a sequence, that sequence value is lost, and that creates a gap in your primary key values.

But whether you worry about these concerns or not, using a separate database sequence for each entity class is a common practice. And Hibernate 6’s new default naming strategy makes that a little easier. If you don’t specify the sequence name, Hibernate 6 adds the postfix _SEQ to the entity’s table name and uses that as the default sequence.

So, starting with version 6, Hibernate uses the sequence ChessPlayer_SEQ when persisting above’s entity class.

16:15:04,917 DEBUG [org.hibernate.SQL] - 
    select
        nextval('ChessPlayer_SEQ')
16:15:04,947 DEBUG [org.hibernate.SQL] - 
    insert
    into
        ChessPlayer
        (birthDate, firstName, lastName, version, id) 
    values
        (?, ?, ?, ?, ?)

If you want to keep using the old default naming strategy, you need to configure the ID_DB_STRUCTURE_NAMING_STRATEGY property in your persistence.xml file.

<persistence>
    <persistence-unit name="my-persistence-unit">
        ...
        <properties>
            <property name="hibernate.id.db_structure_naming_strategy" value="standard" />
            ...
        </properties>
    </persistence-unit>
</persistence>

If you set it to legacy, Hibernate will use the same default strategy as in the Hibernate versions >= 5.3 but <6. And if you set it to single, you get the same default strategy as in Hibernate versions < 5.3.

I explained all of this in more detail in my article about Hibernate 6’s new sequence naming strategies.

4. Incubating new features

The Hibernate team will use the new @Incubating annotation to warn users that a new API or configuration parameter might still change. They want to use that if they release a feature for which they’re looking for further user feedback or if they’re releasing a subset of a set of interconnected features. In that case, some of the missing features might require additional changes on the already released APIs. I listed a few examples of incubating features in a recent article.

Even though this might cause some inconveniences when we’re using these new APIs, I think this can be a good approach. It gives the Hibernate team more flexibility and enables them to release new features earlier.

But before we come to a final conclusion about this approach, we will need to wait and see how often incubating features actually change. When they announced this feature with the release of Hibernate 6.0, the Hibernate teams said they would try to avoid changing incubating APIs and features. So, I’m optimistic that this will only rarely happen.

5. ResultTransformer got replaced

When the ResultTransformer interface got deprecated in Hibernate 5 without offering an alternative, many developers expected that the feature would be removed in a future version. But that’s not the case!

Hibernate 6 split the ResultTransformer interface into the TupleTransformer and the ResultListTransformer interfaces. The goal of that change was to fix a design flaw in the old ResultTransformer interface. It defined 2 methods:

  1. The transformTuple method, which transforms a single record, and
  2. The transformList method, which transforms the entire list of the transformed objects.
Query query = session.createNativeQuery("select id as personId, first_name as firstName, last_name as lastName, city from Person p")
    .setResultTransformer(new ResultTransformer(){
            @Override
            public Object transformTuple(Object[] tuples, String[] aliases) {
                PersonDTO personDTO = new PersonDTO();
                personDTO.setPersonId((int)tuples[0]);
                personDTO.setFirstName((String)tuples[1]);
                personDTO.setLastName((String)tuples[2]);
                return personDTO;
            }
  
            @Override
            public List transformList(List list) {
                return list;
            }
        });
List<PersonDTO> list = query.list();

But almost all implementations only provided a meaningful implementation for one of these methods. So, the team decided to separate the 2 methods and let you choose which one you want to implement.

You can see in the following code snippet that this simplifies the interfaces and enables you to use them as functional interfaces.

PersonDTO person = (PersonDTO) session
        .createQuery("select id as personId, first_name as firstName, last_name as lastName, city from Person p", Object[].class)
        .setTupleTransformer((tuples, aliases) -> {
                log.info("Transform tuple");
                PersonDTO personDTO = new PersonDTO();
                personDTO.setPersonId((int)tuples[0]);
                personDTO.setFirstName((String)tuples[1]);
                personDTO.setLastName((String)tuples[2]);
                return personDTO;
        }).getSingleResult();

As I explained in my guide to Hibernate’s ResultTransformers, the Hibernate team converted all their transformer implementations to the new interfaces. So, the required changes to your code base are minimal.

6. Mapping JSON documents got easier

Hibernate 6 makes storing JSON documents in your database and mapping them to Java objects much easier. In versions 4 and 5, you had to implement a custom UserType that tells Hibernate how to serialize and deserialize the JSON document and how to read and write it to the database. In most cases, all of that is no longer necessary with Hibernate 6.

Starting with version 6, Hibernate can map a JSON document to an @Embeddable. As I show in the Advanced Hibernate course in the Persistence Hub, an embeddable is a reusable mapping component that becomes part of the entity definition and gets mapped to the same database table.

You define an embeddable by implementing a Java class and annotating it with @Embeddable. In this specific case, the embeddable represents the data structure to which you want to map the JSON document.

@Embeddable
public class MyJson implements Serializable {
  
    private String stringProp;
      
    private Long longProp;

    ...
}

After you have defined the embeddable, you can use it as an entity attribute. That requires an additional @Embedded annotation on the attribute. It tells Hibernate to get further mapping information from the embeddable class.

And if you want to map the embeddable to a JSON document, you also need to annotate it with @JdbcTypeCode(SqlTypes.JSON) and include a JSON mapping library in your classpath.

@Entity
public class MyEntity {
  
    @Id
    @GeneratedValue
    private Long id;

    @Embedded  
    @JdbcTypeCode(SqlTypes.JSON)
    private MyJson jsonProperty;
      
    ...
}

Hibernate then serializes and deserializes the attribute to a JSON document and stores it in the database. The type of database column to which Hibernate maps the attribute depends on your database dialect. If you’re using a PostgreSQL database, Hibernate uses a JSONB column.

And as I showed in a recent episode of the Java persistence news, you can use the same approach to map a JSON document to a java.util.Map.

7. Java Records can be embeddables

Since the introduction of records, developers have wanted to use them to model immutable entity classes. Unfortunately, that’s still not possible. But Hibernate 6 supports records as embeddables.

In versions 6.0 and 6.1, you need to implement an EmbeddableInstantiator for each record you want to use as an embeddable. The EmbeddableInstantiator is a proprietary Hibernate interface that tells Hibernate how to instantiate an embeddable object. This removes JPA’s requirement of a no-argument constructor and enables you to model an embeddable as a record.

Starting with version 6.2, Hibernate provides its own EmbeddableInstantiator for all embeddable records.

Let’s take a look at a quick example. You define an embeddable record by annotating your record with an @Embeddable annotation. If you’re using Hibernate 6.0 or 6.1, you also need to annotate it with @EmbeddableInstantiator and reference your instantiator implementation.

@Embeddable
@EmbeddableInstantiator(AddressInstantiator.class)
public record Address (String street, String city, String postalCode) {}

Implementing the EmbeddableInstantiator interface isn’t complex and only necessary if you’re using Hibernate 6.0 or 6.1.

As you can see in the following code snippet, only implementing the instantiate method requires multiple lines of code. Hibernate calls it every time it has to instantiate an embeddable object and provides all the attribute values in a ValueAccess object. That object contains these values in the alphabetical order of the attributes’ names.

public class AddressInstantiator implements EmbeddableInstantiator {
 
    Logger log = LogManager.getLogger(this.getClass().getName());
 
    public boolean isInstance(Object object, SessionFactoryImplementor sessionFactory) {
        return object instanceof Address;
    }
 
    public boolean isSameClass(Object object, SessionFactoryImplementor sessionFactory) {
        return object.getClass().equals( Address.class );
    }
 
    public Object instantiate(ValueAccess valuesAccess, SessionFactoryImplementor sessionFactory) {
        // valuesAccess contains attribute values in alphabetical order
        final String city = valuesAccess.getValue(0, String.class);
        final String postalCode = valuesAccess.getValue(1, String.class);
        final String street = valuesAccess.getValue(2, String.class);
        log.info("Instantiate Address embeddable for "+street+" "+postalCode+" "+city);
        return new Address( street, city, postalCode );
    }
}

8. Improved OffsetDateTime and ZonedDateTime mapping

Hibernate 5 introduced proprietary support for OffsetDateTime and ZondDateTime by converting them into the local timezone of your application before persisting the timestamp without timezone information. When reading the timestamp from the database, Hibernate then added the local timezone to the timestamp.

That mapping works as long as all your Java applications use the same timezone, the timezone never changes and is a timezone without daylight saving time. Most teams either avoided this mapping or used UTC as their timezone.

Hibernate 6 improves this mapping by introducing the @TimeZoneStorage annotating and supporting 5 different mappings:

  • TimeZoneStorageType.NATIVE stores the timestamp in a column of type TIMESTAMP_WITH_TIMEZONE,
  • TimeZoneStorageType.NORMALIZE uses the mapping introduced in Hibernate 5. It normalizes the timestamp to your JDBC driver’s local timezone and persists it without timezone information,
  • TimeZoneStorageType.NORMALIZE_UTC normalizes the timestamp to UTC and persists it without timezone information,
  • TimeZoneStorageType.COLUMN stores the timestamp without timezone information in one column and the difference between the provided timezone and UTC in another column,
  • TimeZoneStorageType.AUTO lets Hibernate choose based on the capabilities of your database. It either uses TimeZoneStorageType.NATIVE or TimeZoneStorageType.COLUMN.

I explained all these different mappings in great detail in my guide to Hibernate 6’s improved ZonedDateTime and OffsetDateTime mapping.

Here you can see a simple example of a ZonedDateTime attribute that Hibernate shall map to a column of type TIMESTAMP_WITH_TIMEZONE using TimeZoneStorageType.NATIVE.

@Entity
public class ChessGame {
     
    @TimeZoneStorage(TimeZoneStorageType.NATIVE)
    private ZonedDateTime zonedDateTime;
     
    ...
}

Conclusion

Migrating your application to Hibernate 6 might require some work because JPA 3 had to change all package and configuration parameter names from javax.persistence to jakarta.persistence. The Hibernate team also removed some APIs that have been deprecated for several years.

But after you have completed the migration, you get access to several new features and simplifications that are worth the effort. As I showed you in the article, mapping JSON documents and handling ZonedDateTime and OffsetDateTime got easier and more flexible. Implementing a TupleTransformer or ResultListTransformer is also easier than the old approach. And with the new default naming strategy for database sequences, the approach used by most teams becomes the new default.

And if you follow my blog or the announcements of the Hibernate team, you know that this is just the beginning. There are many more features and improvements in the making from which you will benefit after migrating your application to Hibernate 6.

If you want to stay up to date with the latest developments in Hibernate and Spring Data JPA, I recommend joining the Persistence Hub. Besides many other things, that gives you access to my monthly Java Persistence News in which I tell you about the latest releases and show you how to use the new features.

Leave a Reply

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

This site uses Akismet to reduce spam. Learn how your comment data is processed.