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

<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