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