Some primary keys consist of more than 1 entity attribute or database column. These are called composite primary keys. They often contain a foreign key reference to a parent object or represent a complex natural key. Another reason to use composite keys is combining a domain-specific value with a simple counter, e.g., an order type and the order number. If this counter gets incremented independently, most developers want to use a database sequence for it.

On the database level, you can model this easily. Your primary key definition references all columns that are part of the primary key. A sequence is an independent object. You request a value during your insert operation and set it as the value of your counter.

The mapping to a JPA entity includes a few challenges that we will solve in this article.

Modelling a composite primary key

If you want to model a JPA entity that uses a composite primary key, you need to provide 1 class representing this key. Your persistence provider and cache implementations use objects of this class internally to identify an entity object. The class has to model all attributes that are part of the primary key. It also needs to have a no-args constructor and implement the Serializable interface and the equals and hashCode methods.

After you implemented that class, you need to decide if you want to use it as an @EmbeddedId or an @IdClass. There are a few important differences between these mappings, which I explain in more detail in my Advanced Hibernate Online Training. In this article, I will only give you a quick introduction to both options. I will also explain why this is one of the few situations in which an @IdClass is your better option.

Mapping an @EmbeddedId

As the name indicates, the mapping of a composite primary key as an @EmbeddedId requires an embeddable. This simple Java class defines a reusable piece of mapping information that becomes part of the entity. If you want to use the embeddable as an identifier, it also needs to fulfill JPA’s previously mentioned requirements of an identifier.

Here you can see a simple example of a ChessGameId embeddable that models the attributes id and tournamentCode. I want to use them as the identifying attributes of my ChessGame entity class.

@Embeddable
public class ChessGameId implements Serializable {

    private Long id;

    private String tournamentCode;

    public ChessGameId() {
    }

    public ChessGameId(Long id, String tournamentCode) {
        this.id = id;
        this.tournamentCode = tournamentCode;
    }

    @Override
    public int hashCode() {
        return Objects.hash(id, tournamentCode);
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (obj == null || getClass() != obj.getClass()) {
            return false;
        }
        ChessGameId chessGameId = (ChessGameId) obj;
        return id.equals(chessGameId.id) && tournamentCode.equals(chessGameId.tournamentCode);
    }
	
    // getter and setter methods
}

The only thing special about this mapping is the @Embeddable annotation. It tells the persistence provider that all attributes and mapping information shall become part of the entity that uses ChessGameId as an attribute type.

Next, I use this embeddable in my ChessGame entity class and annotate it with @EmbeddedId. That tells Hibernate to include all mapped attributes of ChessGameId in this entity and use them as the primary key.

@Entity
public class ChessGame {

    @EmbeddedId
    private ChessGameId chessGameId;

    private LocalDate date;

    private int round;

    @Version
    private int version;

    @ManyToOne(fetch = FetchType.LAZY)
    private ChessTournament chessTournament;

    @ManyToOne(fetch = FetchType.LAZY)
    private ChessPlayer playerWhite;
    
    @ManyToOne(fetch = FetchType.LAZY)
    private ChessPlayer playerBlack;
	
    // getter and setter methods
}

@EmbeddedIds don’t support generated attributes

All of this might look like a straightforward mapping of a composite primary key. And that would be the case if you don’t want to use a database sequence or an auto-incremented column to generate the primary key value.

The @GeneratedValue annotation is supposed to be used on an attribute annotated with @Id. But none of the attributes of the ChessGameId class are annotated with that annotation. Due to that, Hibernate ignores the @GeneratedValue annotation in the following code snippet.

@Embeddable
public class ChessGameId implements Serializable {

    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "game_seq")
    @SequenceGenerator(name = "game_seq", sequenceName = "game_seq", initialValue = 100)
    private Long id;

    private String tournamentCode;

    public ChessGameId() {
    }

    public ChessGameId(Long id, String tournamentCode) {
        this.id = id;
        this.tournamentCode = tournamentCode;
    }

    @Override
    public int hashCode() {
        return Objects.hash(id, tournamentCode);
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (obj == null || getClass() != obj.getClass()) {
            return false;
        }
        ChessGameId chessGameId = (ChessGameId) obj;
        return id.equals(chessGameId.id) && tournamentCode.equals(chessGameId.tournamentCode);
    }
	
    // getter and setter methods
}

When you persist a new ChessGame entity object, the value of the id attribute stays null.

15:09:29,337 DEBUG SQL:144 - insert into ChessGame (chessTournament_id, date, playerBlack_country, playerBlack_id, playerWhite_country, playerWhite_id, round, version, id, tournamentCode) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
15:09:29,348  WARN SqlExceptionHelper:137 - SQL Error: 0, SQLState: 23502
15:09:29,348 ERROR SqlExceptionHelper:142 - ERROR: null value in column "id" violates not-null constraint

Mapping an IdClass

If you want to map a composite primary key and generate the value of one of its attributes using a sequence or auto-incremented column, you need to use an IdClass. The main difference to the previous mapping is that you model all entity attributes on your entity class. The attributes of the IdClass don’t become part of the entity definition. They only mirror the identifying attributes.

The IdClass itself is a basic Java class. As defined by the JPA specification, it requires a default constructor and has to implement the Serializable interface and the equals and hashCode methods.

public class ChessPlayerId implements Serializable {

    private Long id;

    private String country;

    public ChessPlayerId() {
    }

    public ChessPlayerId(Long id, String country) {
        this.id = id;
        this.country = country;
    }

    @Override
    public int hashCode() {
        return Objects.hash(id, country);
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (obj == null || getClass() != obj.getClass()) {
            return false;
        }
        ChessPlayerId chessPlayerId = (ChessPlayerId) obj;
        return id.equals(chessPlayerId.id) && country.equals(chessPlayerId.country);
    }
}

The type and name of the IdClass’s attributes need to match the attributes of the entity class that you annotated with @Id. Your persistence provider, in my case Hibernate, then keeps both sets of attributes automatically in sync.

After you define your IdClass, you need to annotate your entity class with an @IdClass annotation and reference that class. In contrast to the previous example, the entity class maps all database columns, including those that are part of the identifier. You need to annotate these attributes with an @Id annotation. This is an obvious difference from the previous example. It enables you also to annotate one or more of them with a @GeneratedValue annotation.

@Entity
@IdClass(ChessPlayerId.class)
public class ChessPlayer {

    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "player_seq")
    @SequenceGenerator(name = "player_seq", sequenceName = "player_seq", initialValue = 100)
    private Long id;
    
    @Id
    private String country;

    private String lastName;

    private String firstName;

    private LocalDate birthDate;

    @OneToMany(mappedBy = "playerWhite")
    private Set<ChessGame> gamesWhite;

    @OneToMany(mappedBy = "playerBlack")
    private Set<ChessGame> gamesBlack;

    @Version
    private int version;
	
    // getter and setter methods
}

In this mapping, the id attribute of the ChessPlayer entity class is annotated with an @Id and a @GeneratedValue annotation. The persistence provider no longer ignores the @GeneratedValue annotation and gets a value from the database sequence before persisting a new entity object.

15:42:35,368 DEBUG SQL:144 - select nextval ('player_seq')
15:42:35,388 DEBUG SQL:144 - insert into ChessPlayer (birthDate, firstName, lastName, version, country, id) values (?, ?, ?, ?, ?, ?)

Conclusion

JPA and Hibernate support 2 mappings to model composite primary keys. I generally prefer the mapping of composite primary keys as @EmbeddedIds. But it doesn’t support generated identifier values and can’t be used in this situation. That’s because you can only use the @GeneratedValue annotation on an attribute that’s annotated with @Id. And when using an @EmbeddedId, none of your primary key attributes are annotated with @Id.

Only the mapping as an IdClass supports composite primary keys that use generated identifier values. You model all attributes on your entity class and annotate them with the required mapping annotations. You also need to annotate your entity class with an @IdClass annotation and reference a class that contains all identifying attributes and fulfills JPA’s requirements of an identifier class.