JPA provides four relationship types to model associations between entities. Choosing the right mapping and fetch strategy is critical for performance.

Relationship Types

Annotation Description Example
@OneToOne One entity to one entity User ↔ Profile
@OneToMany / @ManyToOne One to many / many to one Department ↔ Employees
@ManyToMany Many to many Student ↔ Course

One-to-Many / Many-to-One

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

    @OneToMany(mappedBy = "department", cascade = CascadeType.ALL, orphanRemoval = true)
    private List<Employee> employees = new ArrayList<>();

    public void addEmployee(Employee employee) {
        employees.add(employee);
        employee.setDepartment(this);
    }
}

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

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "department_id", nullable = false)
    private Department department;
}
  

Key points:

  • mappedBy on the inverse (non-owning) side
  • @JoinColumn on the owning side (the side with the FK)
  • orphanRemoval = true deletes children when removed from collection

One-to-One

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

    @OneToOne(mappedBy = "user", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
    private UserProfile profile;
}

@Entity
public class UserProfile {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String bio;
    private String avatarUrl;

    @OneToOne
    @JoinColumn(name = "user_id", unique = true)
    private User user;
}
  

Many-to-Many

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

    @ManyToMany
    @JoinTable(
        name = "student_course",
        joinColumns = @JoinColumn(name = "student_id"),
        inverseJoinColumns = @JoinColumn(name = "course_id")
    )
    private Set<Course> courses = new HashSet<>();
}

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

    @ManyToMany(mappedBy = "courses")
    private Set<Student> students = new HashSet<>();
}
  

For large many-to-many relationships, consider a join entity:

  @Entity
@Table(name = "enrollments")
public class Enrollment {
    @EmbeddedId
    private EnrollmentId id;

    @ManyToOne @MapsId("studentId")
    @JoinColumn(name = "student_id")
    private Student student;

    @ManyToOne @MapsId("courseId")
    @JoinColumn(name = "course_id")
    private Course course;

    private LocalDate enrolledDate;
    private Grade grade;
}
  

Cascade Types

Cascade Effect
PERSIST Propagate persist to associated entities
MERGE Propagate merge
REMOVE Propagate delete
ALL All of the above
REFRESH Propagate refresh

Use cascades carefully — only on true parent-child relationships.

Fetch Joins (Avoid N+1)

  // N+1 problem: 1 query for departments + N queries for employees
List<Department> depts = departmentRepository.findAll();

// Solution: fetch join
@Query("SELECT d FROM Department d JOIN FETCH d.employees")
List<Department> findAllWithEmployees();

// Or with EntityGraph
@EntityGraph(attributePaths = {"employees"})
List<Department> findAll();
  

Best Practices

  • Always designate an owning side with @JoinColumn or @JoinTable
  • Use mappedBy on the inverse side to avoid duplicate FK columns
  • Prefer unidirectional relationships when bidirectional is not needed
  • Use fetch joins or @EntityGraph to solve N+1 query problems
  • Consider a join entity for many-to-many with additional attributes