Chapter 2: Relationships between Entities
Introduction
In the real world, data is connected:
- A Customer places multiple Orders
- A Department contains multiple Employees
- A Student takes multiple Courses
This chapter teaches you how to model these relationships with JPA!
What You Will Learn
- The 3 types of relationships (@OneToOne, @OneToMany/@ManyToOne, @ManyToMany)
- Fetch types (LAZY vs EAGER)
- Cascade operations
- Bidirectional relationships
Duration: 1h30
Problem Without Relationships
Naive Approach (Bad)
@Entity
public class Employee {
@Id
private Long id;
private String name;
private Long departmentId; // ❌ Just an ID!
}
@Entity
public class Department {
@Id
private Long id;
private String name;
}
Problems:
// To retrieve an employee's department:
Employee emp = empRepo.findById(1L);
Department dept = deptRepo.findById(emp.getDepartmentId()); // 2 queries!
// To retrieve all employees of a department:
List<Employee> emps = empRepo.findByDepartmentId(deptId); // No relationship!
Solution: JPA Relationships
With Relationships (Good)
@Entity
public class Employee {
@Id
private Long id;
private String name;
@ManyToOne // ✅ JPA Relationship!
private Department department;
}
@Entity
public class Department {
@Id
private Long id;
private String name;
@OneToMany(mappedBy = "department") // ✅ Inverse relationship!
private List<Employee> employees;
}
Advantages:
// Employee's department - EASY!
Employee emp = empRepo.findById(1L);
Department dept = emp.getDepartment(); // Direct access!
// Department's employees - EASY!
Department dept = deptRepo.findById(1L);
List<Employee> emps = dept.getEmployees(); // Direct access!
Type 1: @OneToOne (1-1)
What is it?
An entity linked to ONLY ONE other entity.
Examples:
- User ↔ UserProfile (a user has a profile)
- Person ↔ Passport (a person has a passport)
- Employee ↔ ParkingSpot (an employee has a parking spot)
Code: User and UserProfile
User Entity:
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String username;
private String email;
// OneToOne relationship
@OneToOne(cascade = CascadeType.ALL)
@JoinColumn(name = "profile_id") // Foreign key in users table
private UserProfile profile;
// Constructors, getters, setters
}
UserProfile Entity:
@Entity
public class UserProfile {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String bio;
private String avatar;
// Inverse relationship (optional)
@OneToOne(mappedBy = "profile")
private User user;
// Constructors, getters, setters
}
Usage
// Create
User user = new User("john", "john@email.com");
UserProfile profile = new UserProfile("John's Bio", "avatar.jpg");
user.setProfile(profile);
userRepo.save(user); // Profile saved automatically (cascade)
// Read
User user = userRepo.findById(1L);
UserProfile profile = user.getProfile();
System.out.println(profile.getBio());
Type 2: @ManyToOne and @OneToMany (N-1 / 1-N)
What is it?
Multiple entities linked to ONE entity.
Examples:
- Department ↔ Employees (a department has multiple employees)
- Author ↔ Books (an author writes multiple books)
- Category ↔ Products (a category contains multiple products)
Code: Department and Employee
Many Side (Employee):
@Entity
public class Employee {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private Double salary;
// MANY employees belong to ONE department
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "department_id") // FK in employee table
private Department department;
// Constructors
public Employee() {}
public Employee(String name, Double salary) {
this.name = name;
this.salary = salary;
}
// Getters, setters
}
One Side (Department):
@Entity
public class Department {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
// ONE department has MANY employees
@OneToMany(mappedBy = "department", cascade = CascadeType.ALL)
private List<Employee> employees = new ArrayList<>();
// Constructors
public Department() {}
public Department(String name) {
this.name = name;
}
// Helper methods
public void addEmployee(Employee employee) {
employees.add(employee);
employee.setDepartment(this); // Bidirectional link
}
public void removeEmployee(Employee employee) {
employees.remove(employee);
employee.setDepartment(null);
}
// Getters, setters
}
Usage
// Create a department with employees
Department it = new Department("IT");
it.addEmployee(new Employee("Alice", 50000.0));
it.addEmployee(new Employee("Bob", 60000.0));
deptRepo.save(it); // Employees saved automatically
// Read employees of a department
Department dept = deptRepo.findById(1L);
List<Employee> employees = dept.getEmployees();
employees.forEach(emp -> System.out.println(emp.getName()));
// Read department of an employee
Employee emp = empRepo.findById(1L);
Department dept = emp.getDepartment();
System.out.println(dept.getName());
Type 3: @ManyToMany (N-N)
What is it?
Multiple entities linked to MULTIPLE entities.
Examples:
- Students ↔ Courses (a student takes multiple courses, a course has multiple students)
- Books ↔ Authors (a book has multiple authors, an author writes multiple books)
- Products ↔ Tags (a product has multiple tags, a tag on multiple products)
Requires a join table!
Code: Student and Course
Student Entity:
@Entity
public class Student {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
// MANY students follow MANY courses
@ManyToMany
@JoinTable(
name = "student_course", // Join table
joinColumns = @JoinColumn(name = "student_id"),
inverseJoinColumns = @JoinColumn(name = "course_id")
)
private List<Course> courses = new ArrayList<>();
// Helper methods
public void enrollCourse(Course course) {
courses.add(course);
course.getStudents().add(this);
}
public void dropCourse(Course course) {
courses.remove(course);
course.getStudents().remove(this);
}
// Constructors, getters, setters
}
Course Entity:
@Entity
public class Course {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
// Inverse relationship
@ManyToMany(mappedBy = "courses")
private List<Student> students = new ArrayList<>();
// Constructors, getters, setters
}
Generated Table:
CREATE TABLE student_course (
student_id BIGINT,
course_id BIGINT,
PRIMARY KEY (student_id, course_id)
);
Usage
// Create
Student alice = new Student("Alice");
Course java = new Course("Java Programming");
Course spring = new Course("Spring Boot");
alice.enrollCourse(java);
alice.enrollCourse(spring);
studentRepo.save(alice);
// Read courses of a student
Student student = studentRepo.findById(1L);
List<Course> courses = student.getCourses();
// Read students of a course
Course course = courseRepo.findById(1L);
List<Student> students = course.getStudents();
Fetch Types: LAZY vs EAGER
The Problem
When to load related entities?
Employee emp = empRepo.findById(1L);
// Question: Is the Department loaded now?
EAGER (Immediate Loading)
@ManyToOne(fetch = FetchType.EAGER)
private Department department;
Behavior:
Employee emp = empRepo.findById(1L);
// Department is ALREADY loaded!
Department dept = emp.getDepartment(); // No additional query
Generated SQL:
SELECT e.*, d.*
FROM employee e
LEFT JOIN department d ON e.department_id = d.id
WHERE e.id = 1;
Advantages:
- ✅ Data immediately available
- ✅ No additional query
Disadvantages:
- ❌ Loads data even if not used
- ❌ Can be slow if many relationships
LAZY (Deferred Loading) - Recommended
@ManyToOne(fetch = FetchType.LAZY)
private Department department;
Behavior:
Employee emp = empRepo.findById(1L);
// Department NOT yet loaded (proxy)
Department dept = emp.getDepartment(); // Query NOW
Generated SQL:
-- 1st query
SELECT * FROM employee WHERE id = 1;
-- 2nd query (only if accessing department)
SELECT * FROM department WHERE id = ?;
Advantages:
- ✅ Loads only if necessary
- ✅ Better overall performance
Disadvantages:
- ⚠️ Additional query if accessed
- ⚠️ Can cause N+1 problem
When to Use What?
// ✅ LAZY for @OneToMany (list)
@OneToMany(fetch = FetchType.LAZY) // Default
private List<Employee> employees;
// ✅ LAZY for @ManyToMany (list)
@ManyToMany(fetch = FetchType.LAZY) // Default
private List<Course> courses;
// ⚠️ LAZY for @ManyToOne (recommended)
@ManyToOne(fetch = FetchType.LAZY)
private Department department;
// ⚠️ EAGER only if ALWAYS used
@ManyToOne(fetch = FetchType.EAGER)
private Department department; // Only if always necessary
Golden Rule: Use LAZY by default!
Cascade Operations
The Problem
What happens when I save/delete an entity with relationships?
Department dept = new Department("IT");
dept.addEmployee(new Employee("Alice"));
deptRepo.save(dept);
// Question: Is the employee saved too?
Cascade Types
public enum CascadeType {
PERSIST, // Cascade save
MERGE, // Cascade update
REMOVE, // Cascade delete
REFRESH, // Cascade refresh
DETACH, // Cascade detach
ALL // All operations
}
Cascade.PERSIST (Save)
@OneToMany(mappedBy = "department", cascade = CascadeType.PERSIST)
private List<Employee> employees;
Behavior:
Department dept = new Department("IT");
dept.addEmployee(new Employee("Alice"));
deptRepo.save(dept);
// ✅ Employee saved automatically!
Cascade.REMOVE (Delete)
@OneToMany(mappedBy = "department", cascade = CascadeType.REMOVE)
private List<Employee> employees;
Behavior:
deptRepo.deleteById(1L);
// ⚠️ ALL employees of the department are deleted!
Warning: Use with caution!
Cascade.ALL (Everything)
@OneToMany(mappedBy = "department", cascade = CascadeType.ALL)
private List<Employee> employees;
Behavior:
// Save, update, delete in cascade
deptRepo.save(dept); // Employees saved
deptRepo.delete(dept); // Employees deleted
Recommendations
// ✅ GOOD - Parent owns children
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
private List<OrderItem> items; // OrderItems belong to Order
// ⚠️ CAUTION - Shared entities
@ManyToOne(cascade = CascadeType.PERSIST)
private Department department; // NO REMOVE!
// ❌ DANGEROUS
@ManyToMany(cascade = CascadeType.REMOVE)
private List<Course> courses; // Would delete courses!
orphanRemoval
What is it?
Automatically delete entities that are no longer referenced.
@OneToMany(mappedBy = "department", orphanRemoval = true)
private List<Employee> employees;
Behavior:
Department dept = deptRepo.findById(1L);
dept.getEmployees().remove(0); // Remove from list
deptRepo.save(dept);
// ✅ Employee is DELETED from DB!
Without orphanRemoval:
// ❌ Employee remains in DB with department_id = NULL
Bidirectional Relationships
What is it?
Both entities know each other.
// Employee knows its Department
@ManyToOne
private Department department;
// Department knows its Employees
@OneToMany(mappedBy = "department")
private List<Employee> employees;
Synchronize Both Sides
Recommended helper methods:
@Entity
public class Department {
@OneToMany(mappedBy = "department")
private List<Employee> employees = new ArrayList<>();
// ✅ Helper to synchronize
public void addEmployee(Employee employee) {
employees.add(employee);
employee.setDepartment(this); // Synchronize!
}
public void removeEmployee(Employee employee) {
employees.remove(employee);
employee.setDepartment(null); // Synchronize!
}
}
Usage:
Department dept = new Department("IT");
Employee emp = new Employee("Alice");
// ✅ GOOD - Use the helper
dept.addEmployee(emp);
// ❌ BAD - Forget to synchronize
dept.getEmployees().add(emp); // Forgot emp.setDepartment(dept)!
Chapter Summary
The 3 Types of Relationships
| Type | Annotation | Example | Join Table? |
|---|---|---|---|
| 1-1 | @OneToOne | User ↔ Profile | No |
| N-1 | @ManyToOne | Employee → Department | No |
| 1-N | @OneToMany | Department → Employees | No |
| N-N | @ManyToMany | Student ↔ Course | Yes |
Fetch Types
EAGER: Load immediately (JOIN)
LAZY: Load on demand (recommended)
Cascade
PERSIST: Cascade save
REMOVE: Cascade delete
ALL: All operations
Self-Assessment
- What annotation for "a department has multiple employees"?
- What is the difference between LAZY and EAGER?
- What does
orphanRemoval = truedo? - Why synchronize bidirectional relationships?
Answers:
@OneToManyon Department,@ManyToOneon Employee- LAZY loads on demand, EAGER loads immediately
- Deletes entities removed from the collection
- To keep both sides of the relationship consistent
Next Step
You mastered relationships! Move on to Custom Queries with @Query.