🗃️ JPA Q13 / 64

How do you implement composite keys using Spring Data JPA repositories?

AI-Powered Answer ✓ Answered

Composite keys, also known as primary keys composed of multiple columns, are essential when a single column is insufficient to uniquely identify a record in a database table. In Spring Data JPA, implementing composite keys involves creating an `@Embeddable` class to represent the key and using `@EmbeddedId` in the main entity to reference it. This guide outlines the steps to correctly set up composite keys with Spring Data JPA repositories.

1. Define the Composite Key Class

First, create a separate class that represents your composite primary key. This class must implement Serializable, be annotated with @Embeddable, and define the fields that constitute the composite key. It should also override equals() and hashCode() methods for proper comparison and collection usage.

java
import java.io.Serializable;
import java.util.Objects;
import jakarta.persistence.Embeddable; // For JPA 3.x+
// import javax.persistence.Embeddable; // For JPA 2.x

@Embeddable
public class EmployeeProjectId implements Serializable {

    private Long employeeId;
    private Long projectId;

    public EmployeeProjectId() {}

    public EmployeeProjectId(Long employeeId, Long projectId) {
        this.employeeId = employeeId;
        this.projectId = projectId;
    }

    // Getters and Setters
    public Long getEmployeeId() {
        return employeeId;
    }

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

    public Long getProjectId() {
        return projectId;
    }

    public void setProjectId(Long projectId) {
        this.projectId = projectId;
    }

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

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

2. Use the Composite Key in the Entity

Next, integrate this composite key class into your main entity. Annotate a field of your composite key type with @EmbeddedId. This tells JPA that this field represents the primary key, and its constituent parts map to columns in the entity's table.

java
import jakarta.persistence.EmbeddedId; // For JPA 3.x+
import jakarta.persistence.Entity;
import jakarta.persistence.Table;
// import javax.persistence.EmbeddedId; // For JPA 2.x
// import javax.persistence.Entity;
// import javax.persistence.Table;

@Entity
@Table(name = "employee_projects")
public class EmployeeProject {

    @EmbeddedId
    private EmployeeProjectId id;

    private String role;
    private Integer hoursAllocated;

    public EmployeeProject() {}

    public EmployeeProject(EmployeeProjectId id, String role, Integer hoursAllocated) {
        this.id = id;
        this.role = role;
        this.hoursAllocated = hoursAllocated;
    }

    // Getters and Setters
    public EmployeeProjectId getId() {
        return id;
    }

    public void setId(EmployeeProjectId id) {
        this.id = id;
    }

    public String getRole() {
        return role;
    }

    public void setRole(String role) {
        this.role = role;
    }

    public Integer getHoursAllocated() {
        return hoursAllocated;
    }

    public void setHoursAllocated(Integer hoursAllocated) {
        this.hoursAllocated = hoursAllocated;
    }
}

3. Create the Spring Data JPA Repository

Finally, define your Spring Data JPA repository. The generic type arguments for JpaRepository should be your entity class and your composite key class. Spring Data JPA will automatically understand how to handle operations based on this composite key.

java
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface EmployeeProjectRepository extends JpaRepository<EmployeeProject, EmployeeProjectId> {
    // You can add custom queries here, e.g.,
    // Optional<EmployeeProject> findById_EmployeeIdAndId_ProjectId(Long employeeId, Long projectId);
}

4. Example Usage

To interact with entities that have composite keys, you simply pass an instance of your composite key class to repository methods that expect an ID, such as findById(). When creating a new entity, instantiate the composite key and set it on the entity.

java
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
import java.util.Optional;

@Component
public class EmployeeProjectInitializer implements CommandLineRunner {

    @Autowired
    private EmployeeProjectRepository repository;

    @Override
    public void run(String... args) throws Exception {
        // 1. Create a composite key instance
        EmployeeProjectId key1 = new EmployeeProjectId(1L, 101L);
        EmployeeProjectId key2 = new EmployeeProjectId(2L, 102L);

        // 2. Create an entity with the composite key
        EmployeeProject project1 = new EmployeeProject(key1, "Developer", 160);
        EmployeeProject project2 = new EmployeeProject(key2, "QA Engineer", 120);

        // 3. Save the entity
        repository.save(project1);
        repository.save(project2);
        System.out.println("Saved EmployeeProject: " + project1.getId().getEmployeeId() + ", " + project1.getId().getProjectId());
        System.out.println("Saved EmployeeProject: " + project2.getId().getEmployeeId() + ", " + project2.getId().getProjectId());

        // 4. Retrieve an entity by its composite key
        Optional<EmployeeProject> retrievedProject = repository.findById(key1);
        retrievedProject.ifPresent(p ->
            System.out.println("Retrieved Employee: " + p.getId().getEmployeeId() +
                               " Project: " + p.getId().getProjectId() +
                               " Role: " + p.getRole())
        );

        // 5. Try to retrieve a non-existent entity
        EmployeeProjectId nonExistentKey = new EmployeeProjectId(99L, 999L);
        Optional<EmployeeProject> notFoundProject = repository.findById(nonExistentKey);
        if (notFoundProject.isEmpty()) {
            System.out.println("EmployeeProject with key " + nonExistentKey.getEmployeeId() + ", " + nonExistentKey.getProjectId() + " not found.");
        }
    }
}