This tutorial will walk you through the steps of mapping composite primary key with @IdClass in Hibernate one-to-many relationship, Spring Boot, Spring Data JPA, Lombok, and MySQL
What you need
JDK 8+ or OpenJDK 8+
Maven 3+
MySQL Server 5+
Your favorite IDE
Init project structure
You can create and init a new Spring Boot project by using Spring Initializr or your IDE
Following is the final project structure with all the files we would create
├── src
│ └── main
│ ├── java
│ │ └── com
│ │ └── hellokoding
│ │ └── jpa
│ │ ├── Application.java
│ │ ├── Employee.java
│ │ ├── EmployeePhone.java
│ │ ├── EmployeePhoneId.java
│ │ └── EmployeeRepository.java
│ └── resources
│ └── application.properties
└── pom.xml
The composite primary key would be implemented in EmployeePhoneId.java and used in EmployeePhone.java via @IdClass
Project dependencies
We use the following dependencies
spring-boot-starter-data-jpa provides Hibernate and autoconfigure Spring DataSource
mysql-connector-java provides MySQL Java Client
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
The composite primary key in a one-to-many relationship
Consider the relationship between employee and employee_phone tables: one employee may have multiple phone numbers

The employee_id and phone are the composite primary key of table employee_phone, employee_id is a foreign key
Define JPA and Hibernate Entities
Create Employee and EmployeePhone JPA Entities corresponding to the employee and employee_phone tables in the database
[Employee.java]
package com.hellokoding.jpa;
import lombok.Getter;
import lombok.Setter;
import javax.persistence.*;
import java.util.Set;
@Getter @Setter
@Entity
public class Employee {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
private String name;
@OneToMany(mappedBy = "employee", cascade = CascadeType.ALL)
private Set<EmployeePhone> employeePhones;
public Employee(String name, Set<EmployeePhone> employeePhones) {
this.name = name;
this.employeePhones = employeePhones;
for (EmployeePhone employeePhone: employeePhones) {
employeePhone.setEmployee(this);
}
}
}
[EmployeePhone.java]
package com.hellokoding.jpa;
import lombok.Getter;
import lombok.Setter;
import javax.persistence.*;
@Getter @Setter
@Entity
@IdClass(EmployeePhoneId.class)
public class EmployeePhone {
@ManyToOne
@PrimaryKeyJoinColumn
private Employee employee;
@Id
private String phone;
private Boolean isPrimary;
public EmployeePhone(String phone, Boolean isPrimary) {
this.phone = phone;
this.isPrimary = isPrimary;
}
}
@Entity annotation is required to specify a JPA and Hibernate entity
@Id annotation is required to specify the identifier property of the entity
@OneToMany and @ManyToOne defines a bidirectional one-to-many relationship between 2 entities
@JoinColumn defines a foreign key column
mappedBy value points to the relationship owner
@IdClass specify a composite primary key class
The EmployeePhoneId class is defined as below
[EmployeePhoneId.java]
package com.hellokoding.jpa;
import lombok.Data;
import java.io.Serializable;
@Data
public class EmployeePhoneId implements Serializable {
private Employee employee;
private String phone;
public EmployeePhoneId() {
}
}
Requirements for a composite primary key class
Field name and type have to be the same as the main class
Implements Serializable
Implements no-arguments constructor
Implements equals and hashCode
Spring Data JPA Repository
Spring Data JPA contains some built-in Repository abstracting common functions based on EntityManager to work with database such as findAll, findById, save, delete, deleteById. All we need for this example is extends JpaRepository.
[EmployeeRepository.java]
package com.hellokoding.jpa;
import org.springframework.data.jpa.repository.JpaRepository;
public interface EmployeeRepository extends JpaRepository<Employee, Integer>{
}
Application Properties
Configure the Spring Datasource JDBC URL, user name, and password of your local MySQL server in application.properties
spring.datasource.url=jdbc:mysql://localhost:3306/test?useSSL=false
spring.datasource.username=root
spring.datasource.password=hellokoding
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.jpa.hibernate.ddl-auto=create
spring.jpa.database-platform=org.hibernate.dialect.MySQL57Dialect
spring.jpa.generate-ddl=true
spring.jpa.show-sql=true
Create the test database in your local MySQL server if not exists
We don't have to create table schemas, the ddl-auto=create config allows JPA and
Hibernate does that based on the entity-relationship mappings. In practice, consider to use ddl-auto=none (default) and use a migration tool such as Flyway for better database management
spring.jpa.show-sql=true for showing generated SQL queries in the application logs, consider to disable it on production environment
Creating data with JPA and Hibernate
Thanks to CascadeType.ALL, associated entity EmployeePhone will be saved at the same time with Employee without the need of calling its save function explicitly
[Application.java]
package com.hellokoding.jpa;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@SpringBootApplication
class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
@Bean
public CommandLineRunner runner(EmployeeRepository employeeRepository) {
return r -> {
employeeRepository.save(new Employee("tom", Stream.of(
new EmployeePhone("012", true),
new EmployeePhone("013", false)
).collect(Collectors.toSet())));
};
}
}
Run and test
Type the below command at the project root directory
mvn clean spring-boot:run
Access to your local MySQL Server to query the schema and data created by JPA/Hibernate based on your mapping
mysql> describe employee;
+-------+--------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+-------+--------------+------+-----+---------+----------------+
| id | int(11) | NO | PRI | NULL | auto_increment |
| name | varchar(255) | YES | | NULL | |
+-------+--------------+------+-----+---------+----------------+
mysql> show create table employee_phone\G
Create Table: CREATE TABLE `employee_phone` (
`phone` varchar(255) NOT NULL,
`is_primary` bit(1) DEFAULT NULL,
`employee_id` int(11) NOT NULL,
PRIMARY KEY (`employee_id`,`phone`),
CONSTRAINT `FKn0m0jfxdtxshky3888jqv47dq` FOREIGN KEY (`employee_id`) REFERENCES `employee` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
mysql> select * from employee;
+----+------+
| id | name |
+----+------+
| 1 | tom |
+----+------+
mysql> select * from employee_phone;
+-------+------------+-------------+
| phone | is_primary | employee_id |
+-------+------------+-------------+
| 012 | 1 | 1 |
| 013 | 0 | 1 |
+-------+------------+-------------+
Conclusion
In this tutorial, we learned to map composite primary key with @IdClass in Hibernate one-to-many relationship, Spring Boot, Spring Data JPA, Lombok, and MySQL. You can find the source code on Github