On this page
Entity Relationships
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:
mappedByon the inverse (non-owning) side@JoinColumnon the owning side (the side with the FK)orphanRemoval = truedeletes 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
@JoinColumnor@JoinTable - Use
mappedByon the inverse side to avoid duplicate FK columns - Prefer unidirectional relationships when bidirectional is not needed
- Use fetch joins or
@EntityGraphto solve N+1 query problems - Consider a join entity for many-to-many with additional attributes