Java Records as Embeddables with 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.
Since the release of Java’s record feature, I got asked how you could use records with Hibernate. Until Hibernate 6, I had to tell you that JPA and Hibernate only supported records as DTO projections. That finally changed with the release of Hibernate 6.0 and got even easier with Hibernate 6.2. Unfortunately, you still can’t use records to model your entities, but you can at least use them as an @Embeddable.
Before I show you how to define that mapping, I want to quickly summarize what records are and why JPA doesn’t support them. If you’re already familiar with that, feel free to skip the next section and go directly to the modeling section.
Records and why JPA doesn’t support them
The JDK introduced records to provide a carrier for immutable data that’s comfortable to use and easier to declare than a class. They achieved that by reducing the declaration of a record to 3 simple parts:
- the name of the record,
- optional type parameters, and
- a list of record components.
As you can see in the following code snippet, you can provide all of that in just 1 line of code. That declaration defines a record with the name Address and its 3 components of type String with the names street, city, and postalCode.
public record Address (String street, String city, String postalCode) {}
The record class is final and automatically provides all the infrastructure code required by a data class. These are:
- a private final field and a public accessor method for every component. Both share the same name as the component,
- a canonical constructor that matches the record declaration,
- an implementation of the equals method that compares the type and all component values,
- an implementation of the hashCode method that includes all component values,
- an implementation of the toString method that includes all component names and values.
As you can see, a record automatically provides you with all the boilerplate code you previously let your IDE generate. So, it’s understandable that everyone wants to use them to model their entities.
But you might have already recognized that the definition of a record doesn’t fulfill JPA’s requirements of an entity. These are:
- It has to be a top-level class annotated with @Entity.
- It can’t be final.
- It has to provide a public or protected, parameter-less constructor.
- It has to declare an identifier that consists of at least 1 attribute.
As mentioned earlier, a record class is implicitly final and doesn’t provide a parameter-less constructor. That makes it impossible to use it as a JPA entity class. Until now, Hibernate also doesn’t provide any proprietary extensions that enable you to use a record as an entity class. But you can use them to model embeddable classes.
Modeling embeddables as records
In version 6, Hibernate introduced the EmbeddableInstantiator feature that makes the instantiation of an embeddable more flexible. As a side-effect, this also enables you to map a record as an embeddable.
An embeddable is a reusable mapping component that you can use as an attribute type. The embeddable object then becomes part of the entity, doesn’t have its own lifecycle, and gets mapped to the same database table as the entity class.
If the information stored in the embeddable is immutable, modeling it as a record class seems obvious. But the JPA specification also requires it to be a non-final class and define a public, parameter-less constructor. So, if you want to create a specification-compliant entity model, you can’t use records.
If you only need to support Hibernate in at least version 6.0, you can implement a proprietary EmbeddableInstantiator and use a record as an embeddable. And with Hibernate 6.2, you don’t even need to implement that instantiator yourself.
Hibernate >= 6.2
Starting with version 6.2, Hibernate supports record classes as embeddables. You only need to define a record class and annotate it with @Embeddable.
@Embeddable
public record Address (String street, String city, String postalCode) {}
In this example, I want to use the Address record class with the components street, city, and postalCode as an embeddable. This requires an @Embeddable annotation, which is defined by the JPA specification. It tells the persistence provider that this class can be embedded into entity objects.
After you define your embeddable record, you can use it like any other embeddable. You define an entity attribute of that record type and annotate it with @Embedded.
@Entity
public class Author {
@Id
@GeneratedValue
private Long id;
@Embedded
private Address address;
private String firstName;
private String lastName;
...
}
After that, you can use the entity and its attributes to read and write your data. Just keep in mind that the information represented by the record is immutable.
EntityManager em = emf.createEntityManager();
Author a = em.find(Author.class, authorId);
System.out.println(a.getAddress());
17:43:51,155 DEBUG [org.hibernate.SQL] - select a1_0.id,a1_0.street,a1_0.city,a1_0.postalCode,a1_0.firstName,a1_0.lastName,a1_0.version from Author a1_0 where a1_0.id=?
17:43:51,155 TRACE [org.hibernate.orm.jdbc.bind] - binding parameter [1] as [BIGINT] - [1]
Address: Address[street=homeStreet, city=homeCity, postalCode=12345]
Hibernate >= 6.0
As mentioned earlier, modeling an embeddable as a record class requires some extra effort if you’re using Hibernate 6.0 or 6.1. For those Hibernate versions, you need to provide an EmbeddableInstantiator for each embeddable record. Everything else is identical to the code I showed you in the previous section.
The EmbeddableInstantiator enables you to provide the code Hibernate uses to instantiate an embeddable when fetching it from the database. You can use this feature in 2 steps. You need to implement the EmbeddableInstantiator interface and reference it in your embeddable definition.
Let’s first define the embeddable and tell Hibernate to use a custom EmbeddableInstatiator.
@Embeddable
@EmbeddableInstantiator(AddressInstantiator.class)
public record Address (String street, String city, String postalCode) {}
As you can see, the definition of the embeddable looks very similar to the one I showed you in the previous section. The only difference is the @EmbeddableInstantiator annotation. It’s a Hibernate-specific annotation defining which class Hibernate shall call to instantiate the embeddable. In this example, that’s the AddressInstantiator class.
The implementation of that class is simple. It implements the EmbeddableInstantiator interface, which defines 3 methods.
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 );
}
}
The instantiate method is the most important one of the EmbeddableInstantiator interface. Hibernate calls it with a ValueAccess object that contains all attribute values of the embeddable in the alphabetical order of their names. As you can see in the code snippet, you can access them by index and cast them to their specific type. After you extract all parameters, you can use them to instantiate your record object.
Let’s use this embeddable and its instantiator with the same entity class and test case as in the previous section.
@Entity
public class Author {
@Id
@GeneratedValue
private Long id;
@Embedded
private Address address;
private String firstName;
private String lastName;
...
}
EntityManager em = emf.createEntityManager();
Author a = em.find(Author.class, authorId);
System.out.println(a.getAddress());
As the log output shows, Hibernate calls the AddressInstantiator class with all attribute values and instantiates an Address record when it fetches the Author entity from the database.
17:24:59,987 DEBUG [org.hibernate.SQL] - select a1_0.id,a1_0.city,a1_0.postalCode,a1_0.street,a1_0.firstName,a1_0.lastName,a1_0.version from Author a1_0 where a1_0.id=?
17:24:59,987 TRACE [org.hibernate.orm.jdbc.bind] - binding parameter [1] as [BIGINT] - [1]
17:24:59,991 INFO [org.thoughts.on.java.model.AddressInstantiator] - Instantiate Address embeddable for homeStreet 12345 homeCity
17:24:59,992 INFO [org.thoughts.on.java.model.AddressInstantiator] - Instantiate Address embeddable for homeStreet 12345 homeCity
Address [city=homeCity, postalCode=12345, street=homeStreet]
Conclusion
Java records are simple carrier classes for immutable data. That makes them look like an obvious candidate for entity classes and embeddables.
But the JPA specification requires entity and embeddable classes to be non-final and to provide a parameter-less constructor. A record class doesn’t fulfill these requirements. It’s implicitly final and provides a constructor with a parameter for each record component. Due to that, the JPA specification only allows you to use record classes as DTO projections.
Hibernate 6 is more flexible. It allows you to use final classes for embeddables and enables you to customize their instantiation. This enables you to use records as embeddables.
If you’re using version 6.0 or 6.1, you need to implement your own EmbeddableInstantiator implementation for each embeddable record class. Since version 6.2, that class is no longer required. Hibernate now finds and uses the constructor provided by your embeddable record class.