Understanding Hibernate 6: Crafting Custom Primary Keys
Written on
Introduction
In certain scenarios, designing custom representations for primary keys is essential when utilizing Hibernate. A key reason for this need arises when a primary key consists of multiple attributes, resulting in a composite primary key.
The most intuitive approach to represent this in an entity class involves using several fields annotated with @Id. Additionally, a separate class should be created containing fields corresponding to the identifier attributes of the entity. Each ID class must also override equals() and hashCode() methods.
The most straightforward and efficient way to implement this in Hibernate 6 is by leveraging Java Record along with mapping it as either @IdClass or @EmbeddedId. This method is advantageous due to the record's simplicity, immutability (similar to a primary key), and its inherent equals and hashCode methods.
Application Setup
Project Technical Stack
- Spring Boot 3.3.0
- Java 21
The following dependencies will be configured in the pom.xml file. We will incorporate the Spring Data JPA dependency, and utilize the H2 in-memory database for our application.
<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 includes all necessary Hibernate dependencies.
Mapping with @IdClass Identifier
Hibernate and the Jakarta Persistence specification necessitate each primary key attribute in the entity class to be represented with a single primary key object. One way to accomplish this is by utilizing an IdClass.
The @IdClass annotation defines a composite primary key type, with its fields or properties mapping to the properties of the annotated entity class. Starting from Hibernate ORM 6.5.0, you can implement the IdClass using a Record.
Let’s define the entity class Student with primary key fields: name, age, and address. These three attributes will be annotated with the @Id annotation to designate 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;
}
It is crucial to reference the IdClass using the @IdClass annotation. The primary key fields in the entity must be annotated with @Id, and the Record class defined 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 to be utilized for that entity. The StudentId record should represent 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 illustrated, the attributes of the IdClass StudentId are defined in the same order as the @Id fields in the Student entity.
Inconsistencies with @IdClass when Implemented as a Record
Hibernate internally employs an EmbeddableInstantiator to create a record that signifies the primary key value. This can impose a considerable limitation on how the IdClass record is structured.
When Hibernate instantiates a new IdClass record, the default EmbeddableInstantiator assigns the primary key attribute values in alphabetical order according to their attribute names.
Hence, if the attributes of the record are not arranged alphabetically, the mapping between the record class attributes and the primary key attributes assigned by the EmbeddableInstantiator will become misaligned, leading to erroneous functionality of the findById method.
The appropriate way to define StudentId is to arrange attributes in alphabetical order:
public record StudentId(String address,
Integer age,
String name)
{}
Let’s take a look at the Hibernate logs after rearranging the order of attributes:
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 with @EmbeddedId Identifier
@EmbeddedId is utilized to define a composite primary key that is an Embeddable class or record. The embeddable class must be annotated with @Embeddable, and there should only be one @EmbeddedId annotation, with no @Id annotations present.
A record annotated with @Embeddable is created, encompassing all fields of the composite key. In this example, the StudentId record includes fields for name, age, and address, and is marked as @Embeddable.
@Embeddable
public record StudentId(String name, Integer age, String address) {}
In the Student entity, instead of using a simple @Id, @EmbeddedId is employed to embed the StudentId record. This composite key is linked to the corresponding fields in the Student entity.
@Entity
public class Student {
@EmbeddedId
private StudentId studentId;
private String school;
}
When utilizing the findById query with a StudentId record, the results remain consistent irrespective 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");});
Conclusion
We can define our primary key representation using both @IdClass and @EmbeddedId mappings, particularly when our primary key is composed of multiple attributes.
Following the Jakarta Persistence specification post Hibernate ORM 6.5.0, we can utilize a Record to implement an IdClass.
A record inherently includes equals and hashCode methods but lacks a no-argument constructor. Due to this, Hibernate employs an EmbeddableInstantiator to create an instance of the record for the primary key value. In this process, Hibernate expects the fields of the record to be defined in alphabetical order.
Thank You for Reading
- We appreciate your feedback.
- Stay tuned for more insightful content.