Data with JPAExtra· 45 min read

One-to-One & Many-to-Many Relationships

Beyond one-to-many: sometimes one row pairs with exactly one other (one-to-one), and sometimes many rows link to many rows (many-to-many). JPA handles both with a single annotation each.

What you will learn

  • Model a one-to-one link with @OneToOne
  • Model a many-to-many link with @ManyToMany and a join table
  • Choose the right relationship type for a given pair of entities

The three relationship shapes

Last lesson you modelled one-to-many (one author, many books). That is the most common shape, but two more come up constantly in real apps. Here are all three side by side so you can tell them apart:

ShapeExampleAnnotations
One-to-many / many-to-oneone author → many books@OneToMany + @ManyToOne
One-to-oneone user → one profile@OneToOne
Many-to-manymany students ↔ many courses@ManyToMany

The trick is to ask: “how many of B can one A have, and how many of A can one B have?” If the answer is one each way, it is one-to-one. If it is many each way, it is many-to-many.

One-to-one: a User and its Profile

A one-to-one relationship means each row on one side pairs with exactly one row on the other. A classic example: every User has exactly one Profile (bio, avatar), and each Profile belongs to exactly one User. You split them into two tables to keep the main User row small. Mark the link with @OneToOne:

A one-to-one link between User and Profile
@Entity
public class User {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;

    @OneToOne                      // one user -> one profile
    @JoinColumn(name = "profile_id")   // this table holds the link column
    private Profile profile;
    // constructors, getters, setters ...
}

@Entity
public class Profile {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String bio;
    private String avatarUrl;
    // constructors, getters, setters ...
}

Note: Output: JPA adds a profile_id column to the user table that points to one profile row: user(id, name, profile_id) profile(id, bio, avatar_url) Because @JoinColumn sits on User, the user row stores which profile is its own. Reading user.getProfile().getBio() then loads that single matching profile — no JOIN to write by hand.

Reading it back is just field access, exactly like one-to-many but with a single object instead of a list:

Pairing a User with its Profile and reading it back
Profile profile = new Profile("Java trainer", "ashish.png");
User user = new User("Ashish");
user.setProfile(profile);          // pair the two
profileRepo.save(profile);
userRepo.save(user);

// later: read the profile straight off the user
System.out.println(user.getProfile().getBio());

Note: Output: Java trainer The user knows its one profile, so user.getProfile().getBio() returns "Java trainer". One row on each side, paired exactly once — that is one-to-one.

Many-to-many: Students and Courses

A many-to-many relationship means each side can link to many of the other: one Student enrols in many Course entities, and one Course has many Student entities. A single foreign-key column cannot store “many” on both sides, so JPA uses a separate join table (a small extra table that holds just the pairs of ids). You do not create it by hand — @ManyToMany with @JoinTable tells JPA to build it:

A many-to-many link between Student and Course via a join table
@Entity
public class Student {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;

    @ManyToMany                                  // many students <-> many courses
    @JoinTable(
        name = "student_course",                 // the join table JPA creates
        joinColumns = @JoinColumn(name = "student_id"),
        inverseJoinColumns = @JoinColumn(name = "course_id"))
    private List<Course> courses = new ArrayList<>();
    // constructors, getters, setters ...
}

@Entity
public class Course {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String title;

    @ManyToMany(mappedBy = "courses")            // the other side of the same link
    private List<Student> students = new ArrayList<>();
    // constructors, getters, setters ...
}

Note: Output: JPA builds three tables — the two entities plus a join table that stores only the pairs: student(id, name) course(id, title) studentcourse(studentid, course_id) Each row in student_course is one enrolment, e.g. (1, 7) means student 1 is in course 7. mappedBy = "courses" tells the Course side it shares the same link, so the join table is not duplicated.

Now link them by adding to the list on the owning side, then read the connections from either direction:

Enrolling a student in two courses and counting both ways
Course java = new Course("Spring Boot");
Course db = new Course("Databases");
courseRepo.saveAll(List.of(java, db));

Student asha = new Student("Asha");
asha.getCourses().add(java);       // enrol Asha in two courses
asha.getCourses().add(db);
studentRepo.save(asha);

// read from both directions
System.out.println(asha.getCourses().size());   // how many courses Asha has
System.out.println(java.getStudents().size());  // how many students in Spring Boot

Note: Output: 2 1 Asha is in 2 courses, and the Spring Boot course has 1 student (Asha). JPA wrote two rows into the student_course join table for you — one per enrolment — and reads the links from either side. That is many-to-many.

Which one do I use? A quick decision guide

If one A has…and one B has…use
many Bone A@OneToMany / @ManyToOne
one Bone A@OneToOne
many Bmany A@ManyToMany (join table)

Tip: For @OneToOne and @ManyToMany, pick one owning side — the side with @JoinColumn or @JoinTable — and put mappedBy on the other. The owning side is the one that actually stores the link, which avoids JPA accidentally creating duplicate columns or tables.

Watch out: Just like one-to-many, returning two entities that point back at each other as JSON can cause an endless loop (student → courses → students → courses…). Use @JsonIgnore on one side, or return a DTO (a small plain class carrying only the fields you want to send), to break the cycle when you serialise related data.

Q. Many students can each take many courses, and each course has many students. Which mapping fits?

Answer: “Many on both sides” is a many-to-many relationship, modelled with @ManyToMany. JPA stores the pairs in a separate join table (here student_course), because a single foreign-key column cannot hold many links on both sides.

✍️ Practice

  1. Model a Person with exactly one Passport using @OneToOne, then read the passport off the person.
  2. Model Book and Tag as @ManyToMany (a book has many tags, a tag is on many books) and count both directions.

🏠 Homework

  1. Extend your data model: give each Student one Profile (one-to-one) and let students enrol in many Course entities (many-to-many). Save sample data and print, for one student, their profile bio and the list of course titles they are enrolled in.
Want to learn this with a mentor?

CodingClave runs guided, project-based training (28-day, 45-day & 6-month batches).

Explore Training →