Skip to main content

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​

  1. The 3 types of relationships (@OneToOne, @OneToMany/@ManyToOne, @ManyToMany)
  2. Fetch types (LAZY vs EAGER)
  3. Cascade operations
  4. 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

@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​

TypeAnnotationExampleJoin Table?
1-1@OneToOneUser ↔ ProfileNo
N-1@ManyToOneEmployee → DepartmentNo
1-N@OneToManyDepartment → EmployeesNo
N-N@ManyToManyStudent ↔ CourseYes

Fetch Types​

EAGER: Load immediately (JOIN)
LAZY: Load on demand (recommended)

Cascade​

PERSIST: Cascade save
REMOVE: Cascade delete
ALL: All operations

Self-Assessment​

  1. What annotation for "a department has multiple employees"?
  2. What is the difference between LAZY and EAGER?
  3. What does orphanRemoval = true do?
  4. Why synchronize bidirectional relationships?

Answers:

  1. @OneToMany on Department, @ManyToOne on Employee
  2. LAZY loads on demand, EAGER loads immediately
  3. Deletes entities removed from the collection
  4. To keep both sides of the relationship consistent

Next Step​

You mastered relationships! Move on to Custom Queries with @Query.

➡️ Chapter 3: @Query

⬅️ Previous Chapter: Conventions

⬅️ Back to Module 6