Hibernate Associations
Hibernate Bidirectional Association
Learn how two Hibernate entities reference each other, how the owning side controls the foreign key, and how to keep both sides synchronized.
What Is a Bidirectional Association?
A bidirectional association allows navigation from both entities. For example, an Employee can access its Address, and the Address can access its Employee. Both classes hold a reference, but only one side normally owns the database relationship.
This differs from a unidirectional association, where only one entity has a reference to the other. Bidirectional navigation is useful when both directions are needed by the domain or queries.
How Bidirectional Mapping Works
- Each entity declares a field referencing the other entity or collection.
- Annotations or XML define the relationship type.
- One side is the owning side and maps the foreign key or join table.
- The other side uses
mappedByfor a bidirectional association. - Application helper methods update both object references together.
Example: Employee and Address
In this one-to-one example, Address owns the foreign key and Employee points to the owning field with mappedBy.
@Entity
public class Employee {
@Id
@GeneratedValue
private Long id;
private String name;
@OneToOne(
mappedBy = "employee",
cascade = CascadeType.ALL,
orphanRemoval = true
)
private Address address;
public void setAddress(Address address) {
this.address = address;
if (address != null) {
address.setEmployee(this);
}
}
}
@Entity
public class Address {
@Id
@GeneratedValue
private Long id;
private String city;
private String state;
@OneToOne
@JoinColumn(name = "employee_id", unique = true)
private Employee employee;
public void setEmployee(Employee employee) {
this.employee = employee;
}
}Calling employee.setAddress(address) updates both in-memory references. Without this synchronization, the Java object graph can disagree with the database state.
Types of Bidirectional Associations
| Relationship | Typical mapping | Owning side |
|---|---|---|
| One-to-one | @OneToOne on both entities | Side with @JoinColumn or @MapsId |
| One-to-many / many-to-one | @OneToMany(mappedBy = "parent") and @ManyToOne | Usually the many side with the foreign key |
| Many-to-many | @ManyToMany with @JoinTable | Side declaring the join table |
Bidirectional One-to-Many
The child usually owns the foreign key, while the parent collection uses mappedBy.
public class Department {
@OneToMany(mappedBy = "department")
private List<Employee> employees = new ArrayList<>();
public void addEmployee(Employee employee) {
employees.add(employee);
employee.setDepartment(this);
}
}
public class Employee {
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "department_id")
private Department department;
}Bidirectional Many-to-Many
One side defines the join table and the other side refers to it with mappedBy.
public class Question {
@ManyToMany
@JoinTable(name = "question_answer")
private Set<Answer> answers = new HashSet<>();
}
public class Answer {
@ManyToMany(mappedBy = "answers")
private Set<Question> questions = new HashSet<>();
}Use an explicit join entity when the association has attributes such as order, timestamp, role, approval status, or audit information.
Advantages and Trade-offs
Advantages
- Navigate naturally from either side.
- Query relationships from both entities.
- Keep domain objects expressive.
- Represent parent-child ownership clearly.
Trade-offs
- Both sides must remain synchronized.
- Serialization can produce cycles.
- Lazy loading can fail after session close.
- Large collections can cause expensive queries.
Best Practices
- Identify the owning side before writing the mapping.
- Use
mappedByon the inverse side. - Provide add, remove, and set helper methods that update both sides.
- Use lazy loading and explicit fetch joins for required data.
- Do not include associations in
equals()orhashCode(). - Use DTOs or serialization annotations to prevent infinite JSON recursion.
- Use
orphanRemovalonly when the child lifecycle belongs to the parent. - Add database foreign-key, unique, and join-table constraints.
