Relationships & Cascading
@ManyToOne, @OneToMany, @OneToOne and @ManyToMany, the owning side and mappedBy, join columns and join tables, bidirectional helper methods, cascade types, orphanRemoval, default fetch types, equals/hashCode for entities and JSON recursion.
#Introduction
Relationships are where JPA gets powerful — and where most mapping bugs live. The key ideas: which side owns the foreign key, what cascades, and what loads when.
#The Four Relationship Types
| Annotation | Example | Foreign key lives in | Default fetch |
|---|---|---|---|
@ManyToOne | Many employees → one department | The "many" table (employees.department_id) | EAGER |
@OneToMany | One department → many employees | The other side's table | LAZY |
@OneToOne | One user → one profile | Either table | EAGER |
@ManyToMany | Students ↔ courses | A join table | LAZY |
#@ManyToOne — The Most Common Mapping
@Entity
public class Employee {
@Id @GeneratedValue
private Long id;
@ManyToOne(fetch = FetchType.LAZY, optional = false)
@JoinColumn(name = "department_id", nullable = false)
private Department department;
}CREATE TABLE employees (
id BIGINT PRIMARY KEY,
department_id BIGINT NOT NULL REFERENCES departments(id)
);A unidirectional @ManyToOne is often all you need.
#Bidirectional @OneToMany and the Owning Side
@Entity
public class Department {
@Id @GeneratedValue
private Long id;
@OneToMany(mappedBy = "department", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Employee> employees = new ArrayList<>();
public void addEmployee(Employee e) { // helper keeps both sides in sync
employees.add(e);
e.setDepartment(this);
}
public void removeEmployee(Employee e) {
employees.remove(e);
e.setDepartment(null);
}
}| Side | Mapping | Role |
|---|---|---|
Employee.department | @ManyToOne + @JoinColumn | Owning — controls department_id |
Department.employees | @OneToMany(mappedBy = "department") | Inverse — a read-only mirror |
Unidirectional @OneToMany — Avoid
@OneToMany // no mappedBy, no @JoinColumn
private List<Employee> employees;Without mappedBy, Hibernate creates a join table (department_employees) and extra statements. If you need it, add @JoinColumn(name = "department_id") — but a @ManyToOne on the child (optionally bidirectional) is usually the better design.
#@OneToOne
@Entity
public class User {
@Id @GeneratedValue
private Long id;
@OneToOne(mappedBy = "user", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
private Profile profile;
}
@Entity
public class Profile {
@Id
private Long id;
@OneToOne(fetch = FetchType.LAZY)
@MapsId // profile.id = user.id — shared primary key
@JoinColumn(name = "user_id")
private User user;
}#@ManyToMany
@Entity
public class Student {
@Id @GeneratedValue
private Long id;
@ManyToMany
@JoinTable(name = "student_course",
joinColumns = @JoinColumn(name = "student_id"),
inverseJoinColumns = @JoinColumn(name = "course_id"))
private Set<Course> courses = new HashSet<>();
public void enroll(Course c) { courses.add(c); c.getStudents().add(this); }
}
@Entity
public class Course {
@Id @GeneratedValue
private Long id;
@ManyToMany(mappedBy = "courses")
private Set<Student> students = new HashSet<>();
}#Cascade Types
Cascading propagates an EntityManager operation from parent to child.
| Cascade | Propagates |
|---|---|
PERSIST | persist() — saving a new parent saves new children |
MERGE | merge() |
REMOVE | remove() — deleting the parent deletes children |
REFRESH | refresh() |
DETACH | detach() |
ALL | All of the above |
Department d = new Department("R&D");
d.addEmployee(new Employee("Asha"));
d.addEmployee(new Employee("Ravi"));
departmentRepository.save(d); // CascadeType.PERSIST → 3 INSERTs#orphanRemoval vs CascadeType.REMOVE
CascadeType.REMOVE | orphanRemoval = true | |
|---|---|---|
| Deleting the parent | Deletes children | Deletes children |
| Removing a child from the collection | Nothing — the child stays (FK may become null) | Deletes the child |
order.getLines().remove(line); // with orphanRemoval → DELETE FROM order_lines WHERE id=?Use orphanRemoval when a child can't exist without its parent (order lines, invoice items).
#equals and hashCode for Entities
Entities in Sets and collections need sensible equals/hashCode:
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Employee other)) return false;
return id != null && id.equals(other.getId());
}
@Override
public int hashCode() {
return getClass().hashCode(); // constant: stays the same before and after the id is assigned
}- Comparing generated ids is fine if
hashCodedoesn't change when the id is assigned — hence the constant hash. - If the entity has a natural key (ISBN, email), use that instead.
- Don't include lazy associations or collections — it triggers loading and can recurse infinitely.
- Avoid Lombok
@Data/@EqualsAndHashCode/@ToStringon entities for the same reasons.
#JSON Serialization Loops
Serializing a bidirectional entity (Department → employees → department → …) recurses until StackOverflowError. @JsonManagedReference/@JsonBackReference or @JsonIgnore patch it, but the real fix is returning DTOs from APIs instead of entities.
#Interview Questions
Q1What is the owning side of a relationship?
The side that maps the foreign key or join table (@JoinColumn/@JoinTable, no mappedBy). Only changes to the owning side are persisted; the mappedBy side is a read-only mirror.
Q2What does mappedBy do?
It marks the inverse side and names the owning field, telling Hibernate the relationship is already mapped there — so it doesn't create a second foreign key or join table.
Q3What are the default fetch types?
@ManyToOne and @OneToOne: EAGER. @OneToMany and @ManyToMany: LAZY. Best practice is LAZY everywhere and fetching explicitly per use case.
Q4cascade REMOVE vs orphanRemoval?
Both delete children when the parent is deleted; orphanRemoval also deletes a child as soon as it's removed from the parent's collection.
Q5Why avoid CascadeType.ALL on @ManyToMany?
Because the related entities are shared — removing one side would delete entities other rows still reference.
Q6Why use Set rather than List for @ManyToMany?
Hibernate treats an unordered List as a bag; removing an element deletes and re-inserts all join-table rows. A Set allows removing a single row. (Bags also cause MultipleBagFetchException when fetching two at once.)
Q7How do you implement equals/hashCode for entities?
Use a natural key if one exists; otherwise compare non-null ids and return a constant hashCode. Never include lazy associations.
#Key Takeaways
- The owning side (with the FK) is what gets saved; keep both sides in sync with helper methods.
- Make
@ManyToOne/@OneToOneLAZY explicitly. - Cascade parent → child only;
orphanRemovalfor dependent children. @ManyToMany:Set, no REMOVE cascade, or an explicit join entity.- Careful
equals/hashCode; return DTOs, not entity graphs, from APIs.