Unlocking Hibernate 6: The Wonders of Custom Primary Keys
Written on
Introduction
In certain scenarios, you may need to create unique representations of primary keys while utilizing Hibernate. A common motivation for this is the need to form composite primary keys that consist of multiple attributes.
The simplest method to implement this in an entity class involves using several fields annotated with @Id. You will also need to create a separate class containing fields that correspond to the identifier attributes of the entity. Each of these ID classes must override the equals() and hashCode() methods.
The most straightforward and natural approach to implement this in Hibernate 6 is by utilizing Java Record and mapping it as either @IdClass or @EmbeddedId. This is because a record is efficient, easy to implement, immutable (similar to a primary key), and comes with built-in equals and hashCode methods.
Application Setup
Project Technical Stack
- Spring Boot 3.3.0
- Java 21
We will set up the following dependencies in the pom.xml file.
We will employ the Spring Data JPA dependency, and for our application’s database, we will use the H2 in-memory database.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
The Spring Data JPA dependency provides the necessary Hibernate dependencies for our project.
Mapping using @IdClass Identifier
Hibernate and the Jakarta Persistence specification necessitate that each primary key attribute of the entity class is represented by a singular primary key object. One way to achieve this is by providing an IdClass.
The @IdClass annotation designates a composite primary key type, with its fields or properties mapping to the attributes of the annotated entity class.
Starting from Hibernate ORM 6.5.0, it is possible to use a Record to create the IdClass.
Let’s define the entity class Student with primary key fields such as name, age, and address. These three attributes will be annotated with the @Id annotation, marking them as primary key attributes.
@Entity
@IdClass(StudentId.class)
public class Student {
@Id
private String name; // Id attribute Name with order-1
@Id
private Integer age; // Id attribute Age with order-2
@Id
private String address; // Id attribute Address with order-3
private String school;
}
You must reference the IdClass using the @IdClass annotation. The primary key fields in the entity should be annotated with @Id, and the Record class must have fields or properties that align with the entity’s names and types.
The @IdClass annotation on the Student entity specifies StudentId Record as the IdClass for that entity. The record StudentId must model the attribute name of type String, age of type Integer, and address of type String.
public record StudentId(String name, // Id attribute Name with order-1
Integer age, // Id attribute Age with order-2
String address) { // Id attribute Address with order-3
}
As demonstrated above, the attributes of the IdClass StudentId are defined in the same order as the @Id fields in the Student entity.
When executing the findById query with a StudentId record, the result may often be an empty Optional. Let’s delve into the reason for this.
Student student = new Student();
student.setName("RUCHIRA");
student.setAge(20);
student.setAddress("ADDRESS");
student.setSchool("SCHOOL");
studentRepository.save(student);
StudentId studentId = new StudentId("RUCHIRA", 20, "ADDRESS");
Optional<Student> studentOptional = studentRepository.findById(studentId);
studentOptional.ifPresentOrElse(student -> {
log.info("Student: {}", student);}, () -> {
log.info("No Student Information");});
Inconsistencies with @IdClass when Implemented as a Record
Hibernate internally employs an EmbeddableInstantiator to generate a record that represents the primary key value. This can impose significant limitations on how you design your IdClass record.
When Hibernate instantiates a new IdClass record, its default EmbeddableInstantiator assigns the values of the primary key attributes in alphabetical order based on their attribute names.
Consequently, if the attributes of the record are not organized in alphabetical order, the mapping between the record class attributes and the primary key attributes assigned by the EmbeddableInstantiator will become misaligned. This misalignment will lead to the findById method functioning incorrectly.
The proper way to define StudentId is to arrange the attributes in alphabetical order:
public record StudentId(String address,
Integer age,
String name) {
}
Let’s review the Hibernate logs after rearranging the attribute order:
StudentId studentId = new StudentId("ADDRESS", 20, "RUCHIRA");
Optional<Student> studentOptional = studentRepository.findById(studentId);
studentOptional.ifPresentOrElse(student -> {
log.info("Student: {}", student);}, () -> {
log.info("No Student Information");});
Mapping using @EmbeddedId Identifier
The @EmbeddedId annotation is utilized to define a composite primary key that is an Embeddable class or record. The embeddable class must be annotated as @Embeddable, and there should only be a single @EmbeddedId annotation with no @Id annotation.
A record marked with @Embeddable is created, incorporating all fields of the composite key. In this instance, the StudentId record contains fields for name, age, and address, and is designated as Embeddable.
@Embeddable
public record StudentId(String name, Integer age, String address) {
}
In the Student entity, instead of using a plain @Id, we will use @EmbeddedId to include the StudentId record. This composite key is linked with the corresponding fields in the Student entity.
@Entity
public class Student {
@EmbeddedId
private StudentId studentId;
private String school;
}
When executing the findById query with a StudentId record, the results remain consistent, regardless of the order of attributes in the Student record.
StudentId studentId = new StudentId("RUCHIRA", 20, "ADDRESS");
Student student = new Student();
student.setSchool("SCHOOL");
student.setStudentId(studentId);
studentRepository.save(student);
Optional<Student> studentOptional = studentRepository.findById(studentId);
studentOptional.ifPresentOrElse(student -> {
log.info("Student: {}", student);}, () -> {
log.info("No Student Information");});
It is evident that @EmbeddedId provides greater flexibility and does not have the same restrictions encountered with @IdClass when utilized with a record.
Conclusion
We can define our primary key representation using both @IdClass and @EmbeddableId mappings. This is essential when our primary key comprises multiple attributes.
According to the Jakarta Persistence specification following Hibernate ORM 6.5.0, we can implement an IdClass using a Record.
A record inherently includes equals and hashCode methods but lacks a no-argument constructor. Therefore, Hibernate utilizes an EmbeddableInstantiator to create an instance of the record for the primary key value. In doing so, Hibernate expects the fields of the record to be defined in alphabetical order.
Thank You for Reading
- We welcome your feedback.
- Stay tuned for more insightful content.