|

Implement your primary key as a Record using an IdClass


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.


There are various reasons to implement your own primary key representation. Your primary key may consist of multiple attributes. Or you’re following domain-driven design principles and want to use a custom type to add semantic meaning to it.

Implementing a record and mapping it as an IdClass seems evident in these situations. A record is efficient, easy to implement, immutable (just like a primary key), and provides equals and hashCode methods.

And if you map it as an IdClass, it only has to define the same set of fields the entity uses as primary key attributes and implement the equals and hashCode methods. Everything else is handled by Hibernate.

That’s at least the case if you’re using Hibernate in at least version 6.5. Older versions, unfortunately, don’t support records as IdClasses. The reason is that the Jakarta Persistence specification and older Hibernate versions expect your IdClass to provide a parameterless constructor and getter and setter methods for all attributes.

But don’t worry; if you’re using an older Hibernate version, you can still use the IdClass mapping I will show you in this article. You only have to implement it as a standard Java class instead of a record.

Let’s take a look at an example.

Implement an IdClass

A ChessGame entity’s primary key consists of the players’ names and the round in which they played the game. I annotated these 3 attributes with an @Id annotation to mark them as primary key attributes.

@Entity
@IdClass(ChessGameId.class)
public class ChessGame {

    @Id
    private int round;

    @Id
    private String playerWhite;
    
    @Id
    private String playerBlack;

    private ZonedDateTime dateTime;

    @Version
    private int version;

    ...
}

As mentioned earlier, Hibernate and the Jakarta Persistence specification expect that the primary key value of every entity object can be represented by a single primary key object. One way to achieve that is to provide an IdClass.

The IdClass must be referenced by an @IdClass annotation and model all primary key attributes but not others. All IdClass attributes must match their corresponding entity attributes by name and type.

So, in this example, ChessGameId has to model the attribute playerWhite of type String, playerBlack of type String, and round of type int.

Starting with Hibernate ORM 6.5.0, you can implement the IdClass as a record. I did that in the following code snippet. If you’re using an older Hibernate version or have to follow the Jakarta Persistence specification, you must implement it as a standard Java class with a parameterless constructor.

public record ChessGameId(String playerBlack, String playerWhite, int round) {}

Limitations when implementing an IdClass as a Record

Internally, Hibernate uses an EmbeddableInstantiator to instantiate a record representing the primary key value. This causes an important limitation for the design of your IdClass record.

When instantiating a new IdClass record, Hibernate’s default EmbeddableInstantiator provides the values of the primary key attributes in the alphabetical order of their attribute names.

You have to keep that in mind when defining your record and make sure to define its fields in alphabetical order. As you can see in the previous code snippet, I did that when I defined the ChessGameId record.

Work with an IdClass

After you defined your entity and its IdClass, you can use the entity in the same way as any entity with a simple primary key value.

The entity class defines all primary key attributes, and you can set or change them using their setter methods.

ChessGame game = new ChessGame();
game.setRound(1);
game.setPlayerWhite("Fabiano Caruana");
game.setPlayerBlack("Hikaru Nakamura");
game.setDateTime(ZonedDateTime.of(2024, 4, 4, 14, 30, 0, 0, ZoneId.of("America/Toronto")));
em.persist(game);

Hibernate then maps each attribute to a database column.

16:02:34,659 DEBUG [org.hibernate.SQL] - 
    insert 
    into
        ChessGame
        (dateTime, version, playerBlack, playerWhite, round) 
    values
        (?, ?, ?, ?, ?)
16:02:34,662 TRACE [org.hibernate.orm.jdbc.bind] - binding parameter (1:TIMESTAMP_UTC) <- [2024-04-04T14:30-04:00[America/Toronto]]
16:02:34,662 TRACE [org.hibernate.orm.jdbc.bind] - binding parameter (2:INTEGER) <- [0]
16:02:34,662 TRACE [org.hibernate.orm.jdbc.bind] - binding parameter (3:VARCHAR) <- [Hikaru Nakamura]
16:02:34,663 TRACE [org.hibernate.orm.jdbc.bind] - binding parameter (4:VARCHAR) <- [Fabiano Caruana]
16:02:34,663 TRACE [org.hibernate.orm.jdbc.bind] - binding parameter (5:INTEGER) <- [1]

And when you call the EntityManager.find method or use any other of Hibernate’s primary key-based APIs, you have to provide an object of your IdClass. So, in this example, I have to instantiate a ChessGameId record and provide it as a parameter to the EntityManager.find method.

em.find(ChessGame.class, new ChessGameId("Hikaru Nakamura", "Fabiano Caruana", 1));

Hibernate then generates a query with an equal predicate for each part of the primary key.

16:02:34,685 DEBUG [org.hibernate.SQL] - 
    select
        cg1_0.playerBlack,
        cg1_0.playerWhite,
        cg1_0.round,
        cg1_0.dateTime,
        cg1_0.version 
    from
        ChessGame cg1_0 
    where
        (
            cg1_0.playerBlack, cg1_0.playerWhite, cg1_0.round
        ) in ((?, ?, ?))
16:02:34,685 TRACE [org.hibernate.orm.jdbc.bind] - binding parameter (1:VARCHAR) <- [Hikaru Nakamura]
16:02:34,685 TRACE [org.hibernate.orm.jdbc.bind] - binding parameter (2:VARCHAR) <- [Fabiano Caruana]
16:02:34,686 TRACE [org.hibernate.orm.jdbc.bind] - binding parameter (3:INTEGER) <- [1]

Conclusion

You can use an IdClass mapping to define your own primary key representation. This is necessary if your primary key consists of multiple attributes or if you want to model it as a custom type to introduce additional semantic meaning.

Based on the Jakarta Persistence specification and for all Hibernate ORM versions <= 6.4, you have to implement an IdClass as a normal Java class with a parameterless constructor and equals and hashCode methods.

Starting with version 6.5.0, Hibernate ORM also allows you to implement your IdClass as a record. A record already provides an equals and hashCode method but no parameterless constructor. Due to that, Hibernate uses an EmbeddableInstantiator to instantiate a record representing the primary key value. When doing that, it expects that record to define its fields in alphabetical order of their names.