In this article, we will see how to map a composite key with JPA and Hibernate using the annotations that JPA makes available to us.
Composite Key
A composite key is a combination of two or more columns that together form a primary key for a table.
JPA offers us two possibilities to define the mapping of a composite key, through the @IdClass and @EmbeddedId annotations.
In both cases, in order to map a composite key we must implement the Serializable interface and define the equals and hashCode methods.
@IdClass Mapping
In this example we will use a table called task which has two columns, task_type and task_number which together form a composite key.

Now let’s proceed with the mapping by creating a TaskId class with the following properties.
public class TaskId implements Serializable { @Column(name = "task_number") private String taskNumber; @Column(name = "task_type") private String taskType; public TaskId() { } public TaskId(String taskNumber, String taskType) { this.taskNumber = taskNumber; this.taskType = taskType; } // getters, equals and hashCode }
Now it’s time to associate the TaskId class with the Task class.
To do this we need to insert the @IdClass annotation in the Task class, we also need to declare the properties of the TaskId class inside the Task class and annotate them with @Id.
@Entity @Table(name = "task") @IdClass(TaskId.class) public class Task { @Id private String taskNumber; @Id private String taskType; // getters and setters }
@EmbeddedId Mapping
The @EmbeddedId annotation is another possibility to map a composite key.
This time we’ll use a table called employee which has two columns, employee_id and department_id which together form a composite key.

Now, let’s proceed with the mapping of the EmployeeId class.
@Embeddable public class EmployeeId implements Serializable { @Column(name = "employee_id") private Long employeeId; @Column(name = "department_id") private Long departmentId; public EmployeeId() { } public EmployeeId(Long employeeId, Long departmentId) { this.employeeId = employeeId; this.departmentId = departmentId; } // getters, equals and hashCode }
After that, we need to embed this class into Employee class using @EmbeddedId.
@Entity @Table(name = "employee") public class Employee { @EmbeddedId private EmployeeId employeeId; // constructors, getters and setters }
Mapping Relationships Using Composite Key
We can also map relationships using the composite key.
This time we will make use of two tables, one called language with a column code as a primary key and another table called translation which has two columns, translation_id and language_code which together form a composite key however language_code is also a foreign key.

Let’s proceed with the mapping by creating the Language class.
@Entity @Table(name = "language") public class Language { @Id private String code; // getters and setters }
Now to map a composite key with relationship we have two options, we can map it inside the Translation entity or inside the TranslationId class with the @Embeddable annotation.
Relationship Inside Entity
Let’s create the TranslationId class.
@Embeddable public class TranslationId implements Serializable { @Column(name = "translation_id") private Long translationId; @Column(name = "language_code") private String languageCode; public TranslationId() { } public TranslationId(Long translationId, String languageCode) { this.translationId = translationId; this.languageCode = languageCode; } // getters, equals and hashCode }
Now let’s create the Translation class with relationship mapping.
@Entity @Table(name = "translation") public class Translation { @EmbeddedId private TranslationId translationId; @ManyToOne @JoinColumn(name = "language_code", insertable = false, updatable = false) Language language; // getters and setters }
We specified in the @ManyToOne relationship to ignore inserts and updates issued on this mapping since language_code is controlled by @EmbeddedId.
Relationship inside @Embeddable
This time the TranslationId class will be mapped with the @ManyToOne annotation.
@Embeddable public class TranslationId implements Serializable { @Column(name = "translation_id") private Long translationId; @ManyToOne @JoinColumn(name = "language_code") private Language language; public TranslationId() { } public TranslationId(Long translationId, Language language) { this.translationId = translationId; this.language = language; } // getters, equals and hashCode }
Our Translation entity will no longer include the relationship.
@Entity @Table(name = "translation") public class Translation { @EmbeddedId private TranslationId translationId; // getters and setters }
Using @IdClass or @EmbeddedId
The main difference between the @IdClass and @EmbeddedId annotations is that with @IdClass, you need to specify the primary key columns twice: once in the composite primary key class and then again in the entity class with the @Id annotation.
The @EmbeddedId annotation is more detailed than @IdClass because you can access the entire object marked as primary key using the field access method.
Another difference between @IdClass and @EmbeddedId is in the construction of the JPQL query.
For example with @IdClass the construction of the query is a little easier:
SELECT task.taskNumber FROM Task task
While with @EmbeddedId there is a need to write more, for example:
SELECT employee.employeeId.departmentId FROM Employee employee
If we access parts of the composite key individually, we can use @IdClass, but for scenarios where we frequently use the composite key as an object, @EmbeddedId is the better choice.
Conclusion
In this article we have seen how it is possible to map a composite key with JPA and Hibernate, through two different ways. We also saw how a relationship can be mapped to a composite key, and the differences between @IdClass and @EmbeddedId.
The project is available on GitHub.