|

@Incubating features in Hibernate 6


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.


If you tried Hibernate 6, you might have recognized the new @Incubating annotation. The Hibernate team introduces it to tell users about new APIs and interfaces that might still change. That’s a great addition because most developers expect new APIs and features to be stable after they are part of a final release. In general, that is the case. Hibernate’s APIs are incredibly stable. But not being able to change a new API after its 1st release makes it impossible to improve it based on user feedback. It also makes it hard to develop and release a set of interconnected features. So, allowing a little more flexibility might be good for everyone involved.

Incubation marker and report

The new @Incubating annotation gives Hibernate’s development team this flexibility. By annotating a configuration parameter or interface with @Incubating, the team warns their users that this feature might still change. Hibernate 6’s release announcement stated that they, of course, aim to keep these APIs stable. But pending work on other features or user feedback might cause a few changes in the future.

From now on, you should keep an eye on Hibernate’s incubation report and double-check if a newly introduced feature is based on interfaces, classes, or configuration parameters annotated with @Incubating. If that’s the case, you can still use them. But you should be aware that a future release might introduce a change that could affect your code.

@Incubating features in Hibernate 6.0

Hibernate 6.0 includes several features marked as @Incubating. Most of them are SPI’s used to integrate Hibernate in different environments and aren’t relevant for us as application developers. But there are some new features that you should know that are marked as @Incubating.

Configuration parameters for preferred SQL types

Hibernate 6 introduces 4 configuration parameters that you can use to configure the JDBC type that Hibernate shall use to map attributes of type boolean, UUID, Duration, and Instant. You can use them in your persistence.xml configuration and either set them to a numerical JDBC type code or reference the name of a constant defined in org.hibernate.type.SqlTypes.

  • hibernate.type.preferred_boolean_jdbc_type
    sets the JDBC type code for attributes of type boolean. By default, Hibernate gets this type mapping from the database-specific dialect.
  • hibernate.type.preferred_uuid_jdbc_type
    sets the JDBC type code for attributes of type UUID. By default, these get mapped to org.hibernate.types.SqlTypes.UUID, which represents the JDBC type code 3000.
  • hibernate.type.preferred_duration_jdbc_type
    sets the JDBC type code for attributes of type Duration. By default, these get mapped to org.hibernate.types.SqlTypes.INTERVAL_SECOND, which represents the JDBC type code 3100.
  • hibernate.type.preferred_instant_jdbc_type
    sets the JDBC type code for attributes of type Instant. By default, these get mapped to org.hibernate.types.SqlTypes.TIMESTAMP_UTC, which represents the JDBC type code 3003.

Here you can see an example configuration that tells Hibernate to map attributes of type UUID to java.sql.Types.CHAR.

<persistence>
    <persistence-unit name="my-persistence-unit">
        ...
        <properties>
 			...

            <property name="hibernate.type.preferred_uuid_jdbc_type" value="CHAR" />
       </properties>
    </persistence-unit>
</persistence>

If you use this configuration and persist an Author entity that uses an attribute of type UUID as its primary key, you can see in the log output that Hibernate mapped that attribute as type CHAR instead of UUID.

15:24:58,715 DEBUG [org.hibernate.SQL] - insert into Author (city, postalCode, street, firstName, lastName, version, id) values (?, ?, ?, ?, ?, ?, ?)
15:24:58,716 TRACE [org.hibernate.orm.jdbc.bind] - binding parameter [1] as [VARCHAR] - [homeCity]
15:24:58,717 TRACE [org.hibernate.orm.jdbc.bind] - binding parameter [2] as [VARCHAR] - [12345]
15:24:58,717 TRACE [org.hibernate.orm.jdbc.bind] - binding parameter [3] as [VARCHAR] - [homeStreet]
15:24:58,717 TRACE [org.hibernate.orm.jdbc.bind] - binding parameter [4] as [VARCHAR] - [firstName]
15:24:58,717 TRACE [org.hibernate.orm.jdbc.bind] - binding parameter [5] as [VARCHAR] - [lastName]
15:24:58,717 TRACE [org.hibernate.orm.jdbc.bind] - binding parameter [6] as [INTEGER] - [0]
15:24:58,719 TRACE [org.hibernate.orm.jdbc.bind] - binding parameter [7] as [CHAR] - [c4e6a76d-d241-4806-aeae-8afca5598cf2]

Separate query interfaces for reading and writing

The SelectionQuery and MutationQuery interfaces introduced in Hibernate 6.0.0 are marked as @Incubating. They try to improve an unfortunate design decision by the JPA specification and older Hibernate versions.

Based on the JPA specification and previous Hibernate versions, all reading and modifying queries are represented by a Query interface, or its stronger typed version, the TypedQuery interface. If you look at the code that uses these interfaces, you quickly recognize that reading and modifying queries are different. But that’s not obvious when you look at the Query interface. It defines several methods that you can only use with one type of query. Two examples are:

  • the executeUpdate method that executes a modifying statement and
  • the setFirstResult and setMaxResults methods that paginate the result of a selecting statement.

The new SelectionQuery and MutationQuery interfaces separate these responsibilities and provide much cleaner APIs. I described both interfaces in more detail in my Guide to MutationQuery and SelectionQuery in Hibernate 6.

Don’t worry; you don’t need to immediately update your entire application to use the new interfaces. Hibernate 6 still supports the Query and TypedQuery interfaces, and the backward compatibility requirements of the JPA specification make it unlikely that this will change. The Query interface now extends the new SelectionQuery and MutationQuery interfaces. 

The following code snippet shows a simple example of a SelectionQuery. As you can see, this code snippet would look almost the same if I used the standard Query interface instead. The only difference is that I’m calling the createSelectionQuery method instead of the createQuery method to create the query.

SelectionQuery<Book> q = s.createSelectionQuery("SELECT b FROM Book b WHERE b.title = :title", Book.class);
q.setParameter("title", "Hibernate Tips - More than 70 solutions to common Hibernate problems");
List<Book> books = q.getResultList();

The main difference only becomes visible while you’re writing your code. The SelectionQuery interface only defines the methods that you can use on a query that selects data.

And the same is true for a MutationQuery interface. Using this interface, you benefit even more from the separation between read and write operations. You can use most methods defined by the Query interface only on statements that select data from your database. The MutationQuery interface doesn’t define these methods, which provides a cleaner and easier-to-use API.

MutationQuery q = s.createNamedMutationQuery("Book.updateTitle");
q.executeUpdate();

Find out more about the new MutationQuery and SelectionQuery interfaces in my guide to MutationQuery and SelectionQuery in Hibernate 6.

Improved handling of ZonedDateTime and OffsetDateTime

As explained in a previous article, Hibernate 5 normalizes an attribute of type ZonedDateTime or OffsetDate to a configured timezone or the local timezone of your application before it stores it without timezone information in your database. And when you read that attribute’s value from the database, Hibernate adds the configured or local timezone to the timestamp. Even though this approach works fine under the right circumstances, it’s error-prone and inflexible.

Hibernate 6 improved the handling of ZonedDateTime and OffsetDateTime by introducing the @TimeZoneStorage annotation. It enables you to define if you want to:

  • store your timestamp in a column that supports timezone information,
  • store the timezone’s offset in a separate database column,
  • normalize the timestamp to UTC, or
  • normalize the timestamp to a configured or your local timezone.
@Entity
public class ChessGame {
    
    @TimeZoneStorage(TimeZoneStorageType.NORMALIZE_UTC)
    private ZonedDateTime zonedDateTime;
	
	...
	
}

Find out more about Hibernate’s improved mapping of ZonedDateTime and OffsetDateTime.

Refactored result transformer

The ResultTransformer interface was deprecated in Hibernate 5. It defined the transformTuple and transformList methods, which you can implement to tell Hibernate how to map a query result to your preferred data structure. The main issue with this approach was that most transformers only had to implement 1 of the 2 methods and kept the other one empty. Due to that, these 2 methods made the interface unnecessarily complex and prevented us from using it as a functional interface.

In Hibernate 6, the development team split the ResultTransformer interface into the TupleTransformer and ListTransformer interfaces. Each of them defines one of the methods previously defined by the ResultTransformer and can be used as a functional interface.

BookPublisherValue bpv = (BookPublisherValue) session
		.createQuery("SELECT b.title as title, b.publisher.name as publisher FROM Book b WHERE id = 1", Object[].class)
		.setTupleTransformer((tuple, aliases) -> {
				log.info("Transform tuple");
				BookPublisherValue v = new BookPublisherValue();
				v.setTitle((String) tuple[0]);
				v.setPublisher((String) tuple[1]);
				return v;
		}).getSingleResult();

And there is good news if you used one of the ResultTransformer implementations provided by Hibernate 5. In Hibernate 6, you can find the same transformer implementations, which now implement the TupleTransformer or ListTransformer interface.

BookPublisherValue bpv = (BookPublisherValue) session
                .createQuery("SELECT b.title as title, b.publisher.name as publisher FROM Book b WHERE id = 1", Object[].class)
                .setTupleTransformer(new AliasToBeanResultTransformer<BookPublisherValue>(BookPublisherValue.class)).getSingleResult();

I describe all of this in more detail in my guide to Hibernate’s ResultTransformer.

Customized instantiation of embeddables

The EmbeddableInstantiator is another improvement in Hibernate 6, which is marked as incubating. Based on the JPA specification, an embeddable needs to provide a no-arguments constructor. Since Hibernate 6, you can provide an EmbeddableInstantiator instead.

As you can see below, implementing the EmbeddableInstantiator interface isn’t complex. The main part is the implementation of the instantiate method. In that method, you need to get the attribute values in the alphabetical order of their names and use them to instantiate and initialize your embeddable.

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(Supplier<Object[]> valuesAccess, SessionFactoryImplementor sessionFactory) {
        final Object[] values = valuesAccess.get();
        // valuesAccess contains attribute values in alphabetical order
        final String city = (String) values[0];
        final String postalCode = (String) values[1];
        final String street = (String) values[2];
        log.info("Instantiate Address embeddable for "+street+" "+postalCode+" "+city);
        return new Address( street, city, postalCode );
    }

}

If you want to learn more about Hibernate 6’s EmbeddableInstantiator, make sure to read my recent blog post about it.

Conclusion

All developers using Hibernate 6 should know about the @Incubating annotation and the new incubation report. Hibernate’s development team uses them to warn their users about APIs that might change in future releases.

Introducing such an annotation is a great idea because it gives the development team more flexibility when releasing new features and adjusting them until they find a solution that solves most users’ needs. But it also introduces the risk that one of the new features you just started using in your application changes, and you need to adjust your code to it. We will need to see how often that happens and how severe these changes will be.