Spring Boot | Spring Data JPA
Understanding Relationships in JPA: Many-to-Many with Composite Primary Key and Extra Column
Discussing “Many-to-Many with Composite Primary Key and Extra Column” and examining how JPA/Hibernate generates the corresponding tables
As mentioned in the introductory article of this series, I’ve faced difficulties in implementing these relationships correctly in my applications.
To enhance my understanding of them, I developed a GitHub repository called ivangfr/spring-data-jpa-relationships, containing simple examples for each relationship type.
In this series of articles, I will demonstrate each relationship type by presenting the necessary code to map the entities and examining how JPA/Hibernate generates tables.
I hope you find these articles and the GitHub repository helpful.
Today, we will talk about “Many-to-Many with Composite Primary Key and Extra Column”. So, let’s get started!
Many-to-Many with Composite Primary Key and Extra Column
In this example, we will associate Student and Course entities. A student can register to zero, one or more courses and one course can be composed by zero, one or more students. Additionally, it would be interesting to store the date when the student registered for the course and the student's grade in the course.
Below is the desired database model we aim to achieve.

This is the complete and final code. Next, we will explain this code in detail.
@Entity
@Table(name = "courses")
public class Course {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE)
private Long id;
@OneToMany(mappedBy = "course", cascade = CascadeType.ALL)
private Set<CourseStudent> students = new LinkedHashSet<>();
@Column(nullable = false)
private String name;
// getters and setters
}
@Entity
@Table(name = "students")
public class Student {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE)
private Long id;
@OneToMany(mappedBy = "student", cascade = CascadeType.ALL)
private Set<CourseStudent> courses = new LinkedHashSet<>();
@Column(nullable = false)
private String name;
// getters and setters
}
@Entity
@Table(name = "courses_students")
@IdClass(CourseStudentPk.class)
public class CourseStudent {
@Id
@ManyToOne
@JoinColumn(name = "course_id")
private Course course;
@Id
@ManyToOne
@JoinColumn(name = "student_id")
private Student student;
@Column(nullable = false)
private Instant registrationDate = Instant.now();
private Short grade;
// getters and setters
}
public class CourseStudentPk implements Serializable {
private Long course;
private Long student;
// getters and setters
}We have created four classes: Course, Student, CourseStudent, and CourseStudentPk. We use the @Entity annotation in Course, Student and CourseStudentto indicate that these classes correspond to database tables. The @IdClass(CourseStudentPk.class) annotation is used to indicate that the CourseStudent entity uses a composite primary key represented by the CourseStudentPk class.
To customize the table names, we have used the @Table annotation, specifying “courses” for the Course entity, “students” for the Student entity, and “courses_students” for the CourseStudent entity.
In the Course class, we have added the id field of type Long and the name field of type String. Similarly, we have added the id field of type Long and the name field of type String to the Student class. Finally, we have added, to the CourseStudent class, the registrationDate field of type Instant and the grade field of type Short.
To make sure that the name field in the Courser and Student entities, and the registrationDate field in the CourseStudent entity always have values, we add the @Column(nullable = false) annotation. This tells JPA to define the corresponding database column as NOT NULL.
To designate the id field in the Course and Student entities as their primary key, we add the @Id annotation. Additionally, we include the @GeneratedValue(strategy = GenerationType.SEQUENCE) annotation to specify the strategy for generating the ID. For the CourseStudent entity, we are using a composite primary key that is formed by the course and student fields.
The @OneToMany annotation is used in Course and Student entities to establish a one-to-many relationship with the CourseStudent entity. This means that an Course or a Student has a Set of CourseStudent objects. It’s represented by the students field in Course entity and courses field in Student entity.
In Course and Student entities, @OneToMany is used with mappedBy attribute to establish the one-to-many relationship with CourseStudent entity. The cascade attribute is used to define the cascade behavior, which in this case is set to CascadeType.ALL. This means that any changes made to Course or Student entity will be cascaded to the CourseStudent entity.
The CourseStudent entity has @ManyToOne relationships with both the Course and Student entities. It establishes the many-to-one relationship between entities, allowing multiple instances of the CourseStudent entity to be associated with a single Course and a single Student.
The @JoinColumn annotation in the CourseStudent entity specifies the foreign key column name in the courses_students table that references the primary key column of the Course and Student entities.
Lastly, let’s describe the tables in PostgreSQL terminal.
jparelationshipsdb=# \d courses
Table "public.courses"
Column | Type | Collation | Nullable | Default
--------+------------------------+-----------+----------+---------
id | bigint | | not null |
name | character varying(255) | | not null |
Indexes:
"courses_pkey" PRIMARY KEY, btree (id)
Referenced by:
TABLE "courses_students" CONSTRAINT "fkcj1bvqj437mdtgllmwcd41f2u" FOREIGN KEY (course_id) REFERENCES courses(id)
jparelationshipsdb=# \d students
Table "public.students"
Column | Type | Collation | Nullable | Default
--------+------------------------+-----------+----------+---------
id | bigint | | not null |
name | character varying(255) | | not null |
Indexes:
"students_pkey" PRIMARY KEY, btree (id)
Referenced by:
TABLE "courses_students" CONSTRAINT "fkecu9b8uo86fy0wegale3gjtrw" FOREIGN KEY (student_id) REFERENCES students(id)
jparelationshipsdb=# \d courses_students
Table "public.courses_students"
Column | Type | Collation | Nullable | Default
-------------------+-----------------------------+-----------+----------+---------
course_id | bigint | | not null |
student_id | bigint | | not null |
grade | smallint | | |
registration_date | timestamp(6) with time zone | | not null |
Indexes:
"courses_students_pkey" PRIMARY KEY, btree (course_id, student_id)
Foreign-key constraints:
"fkcj1bvqj437mdtgllmwcd41f2u" FOREIGN KEY (course_id) REFERENCES courses(id)
"fkecu9b8uo86fy0wegale3gjtrw" FOREIGN KEY (student_id) REFERENCES students(id)The courses and the students tables have the id column as Primary Key. Additionally, a new table called courses_students was created, whose Primary Key columns are course_id and student_id. The course_id column is a Foreign Key to the column id in the courses table. The same for the student_id column, that is the Foreign Key for the id column in the students table. The extra columns in courses_students, grade and registration_date, are also present.
Conclusion
In this series of articles, we have explored four different types of relationships in JPA: one-to-one, one-to-many / many-to-one, and many-to-many. By presenting simple examples for each type of relationship and providing necessary code to map the entities, we hope to have helped fellow developers gain a better understanding of these relationships.
Support and Engagement
If you enjoyed this article and would like to show your support, please consider taking the following actions:
- 👏 Engage by clapping, highlighting, and replying to my story. I’ll be happy to answer any of your questions;
- 🌐 Share my story on Social Media;
- 🔔 Follow me on: Medium | LinkedIn | Twitter | GitHub;
- ✉️ Subscribe to my newsletter, so you don’t miss out on my latest posts.
