JPA
Hibernate
Composite Key
Mapping
Java Persistence

How to map a composite key with JPA and Hibernate?

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

Composite keys are appropriate when row identity is naturally defined by more than one column, such as student_id plus course_id or tenant_id plus external_id. JPA and Hibernate support this well, but the mapping only stays reliable if the key class is stable, serializable, and aligned with the database schema.

Choose Between @EmbeddedId and @IdClass

JPA gives you two main patterns:

  • '@EmbeddedId keeps the key in one embeddable value object'
  • '@IdClass keeps key fields directly on the entity with a companion key class'

Both work. In practice, @EmbeddedId is usually easier to reason about because the identity is grouped in one place and can be passed around as a value object.

Start with an embeddable key that implements Serializable and defines stable equality.

java
1package com.example.demo;
2
3import jakarta.persistence.Column;
4import jakarta.persistence.Embeddable;
5import java.io.Serializable;
6import java.util.Objects;
7
8@Embeddable
9public class EnrollmentId implements Serializable {
10    @Column(name = "student_id")
11    private Long studentId;
12
13    @Column(name = "course_id")
14    private Long courseId;
15
16    protected EnrollmentId() {
17    }
18
19    public EnrollmentId(Long studentId, Long courseId) {
20        this.studentId = studentId;
21        this.courseId = courseId;
22    }
23
24    public Long getStudentId() {
25        return studentId;
26    }
27
28    public Long getCourseId() {
29        return courseId;
30    }
31
32    @Override
33    public boolean equals(Object o) {
34        if (this == o) return true;
35        if (!(o instanceof EnrollmentId that)) return false;
36        return Objects.equals(studentId, that.studentId)
37                && Objects.equals(courseId, that.courseId);
38    }
39
40    @Override
41    public int hashCode() {
42        return Objects.hash(studentId, courseId);
43    }
44}

Then embed it in the entity.

java
1package com.example.demo;
2
3import jakarta.persistence.EmbeddedId;
4import jakarta.persistence.Entity;
5import jakarta.persistence.Table;
6
7@Entity
8@Table(name = "enrollment")
9public class Enrollment {
10    @EmbeddedId
11    private EnrollmentId id;
12
13    private String grade;
14
15    protected Enrollment() {
16    }
17
18    public Enrollment(EnrollmentId id, String grade) {
19        this.id = id;
20        this.grade = grade;
21    }
22
23    public EnrollmentId getId() {
24        return id;
25    }
26
27    public String getGrade() {
28        return grade;
29    }
30}

This is the cleanest baseline mapping.

Use @MapsId for Relationships

If the composite key includes foreign-key columns, map the relationships explicitly with @MapsId instead of duplicating identifier state in several places.

java
1package com.example.demo;
2
3import jakarta.persistence.EmbeddedId;
4import jakarta.persistence.Entity;
5import jakarta.persistence.JoinColumn;
6import jakarta.persistence.ManyToOne;
7import jakarta.persistence.MapsId;
8
9@Entity
10public class Enrollment {
11    @EmbeddedId
12    private EnrollmentId id;
13
14    @ManyToOne
15    @MapsId("studentId")
16    @JoinColumn(name = "student_id")
17    private Student student;
18
19    @ManyToOne
20    @MapsId("courseId")
21    @JoinColumn(name = "course_id")
22    private Course course;
23
24    private String grade;
25}

This tells JPA that the relationship columns are also part of the embedded primary key. It keeps the entity mapping aligned with the database design.

@IdClass Is Still Valid

If you prefer direct access to id fields on the entity, @IdClass may fit better.

java
1package com.example.demo;
2
3import jakarta.persistence.Entity;
4import jakarta.persistence.Id;
5import jakarta.persistence.IdClass;
6
7@Entity
8@IdClass(EnrollmentId.class)
9public class Enrollment {
10    @Id
11    private Long studentId;
12
13    @Id
14    private Long courseId;
15
16    private String grade;
17}

This style can feel simpler in some queries, but it spreads identity fields across the entity instead of keeping them in one object.

Equality and Mutability Rules Matter

A composite key should behave like a value object. That means:

  • fields should represent immutable identity once persisted
  • 'equals and hashCode should depend only on key fields'
  • the key class should not include mutable business data

If key values can change casually after persistence, entity identity becomes difficult for JPA to manage correctly.

Common Pitfalls

The most common mistake is forgetting equals and hashCode on the key class. Without stable equality, collections, caches, and entity identity behavior become unreliable.

Another issue is duplicating key columns both inside the embedded id and again as separate unmanaged fields. That creates synchronization bugs and confusion about which field is authoritative.

Developers also choose composite keys for convenience when a surrogate key plus a unique constraint would be simpler. A composite key is justified when it represents real domain identity, not just because two columns happen to be unique together today.

Finally, be careful with generated values. Composite primary keys usually do not fit naturally with automatic id generation strategies in the same way single-column surrogate keys do.

Summary

  • JPA supports composite keys with @EmbeddedId and @IdClass.
  • '@EmbeddedId is usually the clearest and most maintainable option.'
  • Use @MapsId when foreign-key relationships are part of the composite key.
  • Keep key classes serializable, stable, and based only on identity fields.
  • Choose a composite key only when the domain identity truly depends on multiple columns.

Course illustration
Course illustration

All Rights Reserved.