Using Composite Key with JPA - spring-boot-in-practice/repo GitHub Wiki

Introduction

In your business domain objects, you can either have a single field as the primary key or you can have a composite key consists of multiple fields. In this quick tutorial, you'll learn how to use multiple fields as the primary key in your Spring Boot application.

Domain Model

To demonstrate this with an example, we'll use the Course-Tracker example we have used in all our techniques. In the examples so far, we've used ID as the primary key. In this example, we'll use the id and name of a course as the primary key.

@Embeddable and @EmbeddedId

The @Embeddable annotation on a class represents the class as a composite primary key. This key is then embedded in the entity class as the composite primary key by using the @EmbeddedId annotation on a field of the @Embeddable type.

Example

Let us create the CourseId class consists of id and name.

package com.manning.sbip.ch03.model;

import javax.persistence.*;
import java.io.Serializable;

@Embeddable
public class CourseId implements Serializable {

    @Column(name = "ID")
    private long id;

    @Column(name = "NAME")
    private String name;

    public CourseId() {
    }

    public CourseId(long id, String name) {
        this.id = id;
        this.name = name;
    }
}

Notice that this class implements the Serializable interface and is annotated with @Embeddable annotation

The next change you'll perform is to include the CourseId class the composite key in the Course class:

package com.manning.sbip.ch03.model;


import javax.persistence.*;
import java.util.Objects;

@Entity
@Table(name = "COURSES")
public class Course {

    @EmbeddedId
    private CourseId courseId;

    @Column(name = "CATEGORY")
    private String category;

    @Column(name = "RATING")
    private int rating;

    @Column(name = "DESCRIPTION")
    private String description;

    public Course() {}

    public Course(CourseId courseId, String category, int rating, String description) {
        this.courseId = courseId;
        this.category = category;
        this.rating = rating;
        this.description = description;
    }

    public CourseId getCourseId() {
        return courseId;
    }

    public void setCourseId(CourseId courseId) {
        this.courseId = courseId;
    }

    public String getCategory() {
        return category;
    }

    public void setCategory(String category) {
        this.category = category;
    }

    public int getRating() {
        return rating;
    }

    public void setRating(int rating) {
        this.rating = rating;
    }

    public String getDescription() {
        return description;
    }

    public void setDescription(String description) {
        this.description = description;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Course)) return false;
        Course course = (Course) o;
        return rating == course.rating &&
                Objects.equals(courseId, course.courseId) &&
                Objects.equals(category, course.category) &&
                Objects.equals(description, course.description);
    }

    @Override
    public int hashCode() {
        return Objects.hash(courseId, category, rating, description);
    }
}

Since the ID type has changed, you need to update the CourseReposiory as well:

package com.manning.sbip.ch03.repository;

import com.manning.sbip.ch03.model.Course;
import com.manning.sbip.ch03.model.CourseId;
import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface CourseRepository extends CrudRepository<Course, CourseId> {
}

Let us now write a unit test to validate the changes:

package com.manning.sbip.ch03;

import com.manning.sbip.ch03.model.Course;
import com.manning.sbip.ch03.model.CourseId;
import com.manning.sbip.ch03.repository.CourseRepository;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import static org.assertj.core.api.Assertions.assertThat;

@SpringBootTest
class CourseTrackerSpringBootApplicationTests {

    @Autowired
    private CourseRepository courseRepository;

    @Test
    public void givenCreateCourseWhenLoadTheCourseThenExpectSameCourse() {
        CourseId courseId = new CourseId(1, "Rapid Spring Boot Application Development");
        Course course = new Course(courseId, "Spring", 4, "'Spring Boot gives all the power of the Spring Framework without all of the complexities");
        courseRepository.save(course);
        assertThat(courseRepository.findById(courseId).get()).isEqualTo(course);
    }
}

If you execute the test case, you'll see it works fine.