# Spring Data JPA Composite Primary Key Mapping Example

By Atta Ur Rehman Shah (https://attacomsian.com/about). Published Oct 19, 2019, updated Sep 25, 2022. Topics: Spring Boot, Spring Data JPA.
Canonical URL: https://attacomsian.com/blog/spring-data-jpa-composite-primary-key

> A complete guide to map a composite primary key by using both @IdClass and @EmbeddedId annotations in Spring Data JPA.

In an [earlier article](https://attacomsian.com/blog/spring-data-jpa-many-to-many-mapping), I wrote about how to map a many-to-many entity relationship using [Spring Data JPA](https://attacomsian.com/blog/getting-started-spring-data-jpa) and MySQL in a Spring Boot project. We created a join table to hold the primary keys of both relationship tables. These keys act as a composite primary for the join database table.

Simply put, a composite primary key, also known as a composite key, is a key that contains two or more columns to form a primary key for the table.

In this article, you'll learn how to map a composite primary key in a Spring Boot project using Spring Data JPA's both `@IdClass` and `@EmbeddedId` annotations.

## Mapping Composite Key using `@IdClass` Annotation

Let us consider an application that manages different types of bank accounts. Each bank account has an account number and type (checking or saving), among other information. We want to create a compound key using this information to uniquely identify each account in the database.

![Spring Data JPA Composite Key with @IdClass Annotation](https://statics.attacomsian.com/gims/577f3757-feb5-48cf-a021-c8f088a4b8f9.png)

In the above Entity-Relationship (ER) diagram, the `accounts` table has a composite primary key, which consists of two columns:

1. `account_number`
2. `account_type`

To map this database relationship using Spring Data JPA, we need to create a separate composite primary key class with both these primary key columns:

<p class="file-desc"><span>AccountId.java</span></p>

```java
package com.attacomsian.jpa.composite.domains;

import java.io.Serializable;
import java.util.Objects;

public class AccountId implements Serializable {

    private String accountNumber;
    private String accountType;

    public AccountId() {
    }

    public AccountId(String accountNumber, String accountType) {
        this.accountNumber = accountNumber;
        this.accountType = accountType;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        AccountId accountId = (AccountId) o;
        return accountNumber.equals(accountId.accountNumber) &&
                accountType.equals(accountId.accountType);
    }

    @Override
    public int hashCode() {
        return Objects.hash(accountNumber, accountType);
    }
}
```

> **Note:** The composite primary key class must be public, contains a no-argument constructor, defines both `equals()` and `hashCode()` methods, and implements the `Serializable` interface.

The next step is to create an `Account` entity class that declares all attributes of `AccountId` and annotates them with the `@Id` annotation:

<p class="file-desc"><span>Account.java</span></p>

```java
package com.attacomsian.jpa.composite.domains;

import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.IdClass;
import javax.persistence.Table;
import java.io.Serializable;

@Entity
@Table(name = "accounts")
@IdClass(AccountId.class)
public class Account implements Serializable {

    @Id
    private String accountNumber;
    @Id
    private String accountType;

    private double balance;

    public Account() {
    }

    public Account(String accountNumber, String accountType, double balance) {
        this.accountNumber = accountNumber;
        this.accountType = accountType;
        this.balance = balance;
    }

    // getters and setters, equals(), toString() .... (omitted for brevity)
}
```

As you can see above, we have annotated the `Account` class with `@IdClass` to specify a composite primary key class that is mapped to multiple fields of the entity. The `@Id` annotation is then used to indicate all properties which are a part of the compound key.

With the `@IdClass` annotation, you can easily query data without using the name of the composite key class. Let us first create a repository interface for this purpose:

<p class="file-desc"><span>AccountRepository.java</span></p>

```java
package com.attacomsian.jpa.composite.repositories;

import com.attacomsian.jpa.composite.domains.Account;
import com.attacomsian.jpa.composite.domains.AccountId;
import org.springframework.data.repository.CrudRepository;

public interface AccountRepository extends CrudRepository<Account, AccountId> {

    // TODO: add queries
}
```

Here is how you can write a simple [derived query](https://attacomsian.com/blog/derived-query-methods-spring-data-jpa) to fetch all bank accounts by a given account type:

```java
List<Account> findByAccountType(String accountType);
```

## Mapping Composite Key using `@EmbeddedId` Annotation

In additional to `@IdClass`, Spring Data JPA provides another annotation &mdash; `@EmbeddedId` &mdash; to define a composite primary key.

Let us consider another example application that manages employees of a company with multiple departments. Each employee has a unique ID within their own department. But the same ID can be assigned to a different employee in another department. So we cannot uniquely identify an employee just by his employee ID.

To uniquely identify an employee, we need to know his employee ID and his department ID. As you can see in the below Entity-Relationship (ER) diagram, the `employees` table contains a composite primary key that includes both `employee_id` and `department_id` columns:

![Spring Data JPA Composite Key with @EmbeddedId Annotation](https://statics.attacomsian.com/gims/50565d0e-0fc7-4f1c-9292-dca18b59c8c6.png)


To map the above relationship using Spring Data JPA, you need to create a separate composite primary key class annotated with `@Embeddable`:

<p class="file-desc"><span>EmployeeId.java</span></p>

```java
package com.attacomsian.jpa.composite.domains;

import javax.persistence.Column;
import javax.persistence.Embeddable;
import java.io.Serializable;
import java.util.Objects;

@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;
    }

    public Long getEmployeeId() {
        return employeeId;
    }

    public void setEmployeeId(Long employeeId) {
        this.employeeId = employeeId;
    }

    public Long getDepartmentId() {
        return departmentId;
    }

    public void setDepartmentId(Long departmentId) {
        this.departmentId = departmentId;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        EmployeeId that = (EmployeeId) o;
        return employeeId.equals(that.employeeId) &&
                departmentId.equals(that.departmentId);
    }

    @Override
    public int hashCode() {
        return Objects.hash(employeeId, departmentId);
    }
}
```

The next step is to create the `Employee` class and embed the above composite primary class into it by using the `@EmbeddedId` annotation:

<p class="file-desc"><span>Employee.java</span></p>

```java
package com.attacomsian.jpa.composite.domains;

import javax.persistence.Column;
import javax.persistence.EmbeddedId;
import javax.persistence.Entity;
import javax.persistence.Table;
import java.io.Serializable;

@Entity
@Table(name = "employees")
public class Employee implements Serializable {

    @EmbeddedId
    private EmployeeId employeeId;

    private String name;
    @Column(unique = true)
    private String email;
    private String phoneNumber;

    public Employee() {
    }

    public Employee(EmployeeId employeeId, String name, String email, String phoneNumber) {
        this.employeeId = employeeId;
        this.name = name;
        this.email = email;
        this.phoneNumber = phoneNumber;
    }

    // getters and setters, equals(), toString() .... (omitted for brevity)
}
```

In the above `Employee` class, we have specified the composite key class using the `@EmbeddedId` annotation and marked it as a primary key of the entity.

Next, create a repository interface for retrieving `Employee` entities from the database, as shown below:

<p class="file-desc"><span>EmployeeRepository.java</span></p>

```java
package com.attacomsian.jpa.composite.repositories;

import com.attacomsian.jpa.composite.domains.Employee;
import com.attacomsian.jpa.composite.domains.EmployeeId;
import org.springframework.data.repository.CrudRepository;

public interface EmployeeRepository extends CrudRepository<Employee, EmployeeId> {

    // TODO: add queries
}
```

Let us create another derived query to fetch all employees by a given department ID:

```java
List<Employee> findByEmployeeIdDepartmentId(Long departmentId);
```

## Testing Composite Primary Key Mapping

Finally, let us create the main application class to test the composite primary key mapping:

<p class="file-desc"><span>Application.java</span></p>

```java
package com.attacomsian.jpa;

import com.attacomsian.jpa.composite.domains.Account;
import com.attacomsian.jpa.composite.domains.AccountId;
import com.attacomsian.jpa.composite.domains.Employee;
import com.attacomsian.jpa.composite.domains.EmployeeId;
import com.attacomsian.jpa.composite.repositories.AccountRepository;
import com.attacomsian.jpa.composite.repositories.EmployeeRepository;
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.List;
import java.util.Optional;

@SpringBootApplication
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }

    @Bean
    public CommandLineRunner mappingDemo(AccountRepository accountRepository,
                                         EmployeeRepository employeeRepository) {
        return args -> {

            // ======= `@IdClass` Annotation =======

            // create new accounts
            accountRepository.save(new Account("458666", "Checking", 4588));
            accountRepository.save(new Account("458689", "Checking", 2500));
            accountRepository.save(new Account("424265", "Saving", 100000));

            // fetch accounts by a given type
            List<Account> accounts = accountRepository.findByAccountType("Checking");
            accounts.forEach(System.out::println);

            // fetch account by composite key
            Optional<Account> account = accountRepository.findById(new AccountId("424265", "Saving"));
            account.ifPresent(System.out::println);

            // ======= `@EmbeddedId` Annotation =======

            // create new employees
            employeeRepository.save(new Employee(new EmployeeId(100L, 10L),
                    "John Doe", "john@example.com", "123456"));
            employeeRepository.save(new Employee(new EmployeeId(101L, 20L),
                    "Emma Ali", "emma@example.com", "654321"));

            // fetch employees by a given department id
            List<Employee> employees = employeeRepository.findByEmployeeIdDepartmentId(20L);
            employees.forEach(System.out::println);

            // fetch employee by composite key
            Optional<Employee> employee = employeeRepository.findById(new EmployeeId(100L, 10L));
            employee.ifPresent(System.out::println);
        };
    }
}
```

In the main application class, we used both `AccountRepository` and `EmployeeRepository` repositories to test our implementation of a composite primary key with `@IdClass` and `@EmbeddedId`. 

The next step is to run the application to see the output. For Gradle, execute the following command to start the application:

```bash
$ ./gradlew bootRun
```

For Maven, type the following command to launch the application:

```bash
$ ./mvnw spring-boot:run
```

When the application starts, you should see the following output printed on the console:

```bash
Account{accountNumber='458666', accountType='Checking', balance=4588.0}
Account{accountNumber='458689', accountType='Checking', balance=2500.0}
Account{accountNumber='424265', accountType='Saving', balance=100000.0}
Employee{employeeId=EmployeeId{employeeId=101, departmentId=20}, name='Emma Ali', email='emma@example.com', phoneNumber='654321'}
Employee{employeeId=EmployeeId{employeeId=100, departmentId=10}, name='John Doe', email='john@example.com', phoneNumber='123456'}
```

## `@IdClass` vs `@EmbeddedId`

The main difference between `@IdClass` and `@EmbeddedId` annotations is that with `@IdClass`, you need to specify the primary key columns twice &mdash; once in the composite primary key class and then again in the entity class with the `@Id` annotation.

The `@EmbeddedId` annotation is more verbose than `@IdClass` as you can access the entire primary key object using the field access method. This also gives a clear notion of the fields part of the composite key because they are all aggregated in a class that is only accessible through a field access method.

Another difference between `@IdClass` and `@EmbeddedId` is when it comes to creating [custom JPQL queries](https://attacomsian.com/blog/spring-data-jpa-query-annotation).

For example, with `@IdClass`, the query is a little simpler:

```java
SELECT a.accountType FROM Account a
```

With `@EmbeddedId`, you have to write more text for a similar query:

```java
SELECT e.employeeId.departmentId FROM Employee e
```

The `@IdClass` annotation can be a preferred choice over `@EmbeddedId` in situations where the composite primary key class is not accessible or comes in from another module or legacy code. For scenarios where you cannot modify the composite key class, the `@IdClass` annotation is the only way out.

> **Source Code:** Download the complete source code from [GitHub](https://github.com/attacomsian/code-examples/tree/master/spring-data-jpa/jpa-mappings) available under MIT license.

## Conclusion

That's all for handling a composite primary key mapping using Spring Data JPA's `@IdClass` and `@EmbeddedId` annotations. You've learned about two different approaches to working with compound keys.

We've also discussed the differences between the `@IdClass` and the `@EmbeddedId` annotations. If we need to access parts of the composite key, just use `@IdClass`, but for scenarios where you frequently use the composite key as an object, `@EmbeddedId` should be preferred.

Don't forget to [subscribe for updates](https://attacomsian.com/newsletter) if you want to be the first to know when new tutorials are available.

## Further Reading

To learn more about Spring Data JPA, check out the following articles:

* [Getting Started with Spring Data JPA](https://attacomsian.com/blog/getting-started-spring-data-jpa)
* [Spring Data JPA with H2 DataBase and Spring Boot](https://attacomsian.com/blog/spring-data-jpa-h2-database)
* [Accessing Data with Spring Data JPA and MySQL](https://attacomsian.com/blog/accessing-data-spring-data-jpa-mysql)
* [Derived Query Methods in Spring Data JPA](https://attacomsian.com/blog/derived-query-methods-spring-data-jpa)
* [Spring Data JPA Custom Queries using @Query Annotation](https://attacomsian.com/blog/spring-data-jpa-query-annotation)
* [How to Use Spring Data JPA Named Queries](https://attacomsian.com/blog/spring-data-jpa-named-queries)
* [Sorting Query Results in Spring Data JPA](https://attacomsian.com/blog/spring-data-jpa-sorting)
* [Pagination with Spring Data JPA](https://attacomsian.com/blog/spring-data-jpa-pagination)
* [Spring Data JPA One To One Relationship Mapping Example](https://attacomsian.com/blog/spring-data-jpa-one-to-one-mapping)
* [Spring Data JPA One To Many Relationship Mapping Example](https://attacomsian.com/blog/spring-data-jpa-one-to-many-mapping)
* [Spring Data JPA Many To Many Relationship Mapping Example](https://attacomsian.com/blog/spring-data-jpa-many-to-many-mapping)
* [Introduction to Spring Data JPA Repositories](https://attacomsian.com/blog/spring-data-jpa-repositories)
