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.

EmployeeAddress

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

  1. Each entity declares a field referencing the other entity or collection.
  2. Annotations or XML define the relationship type.
  3. One side is the owning side and maps the foreign key or join table.
  4. The other side uses mappedBy for a bidirectional association.
  5. Application helper methods update both object references together.
Key idea: two Java references do not mean that Hibernate has two independent relationships. The owning side is the source of truth for updates to the foreign key or join table.

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

RelationshipTypical mappingOwning side
One-to-one@OneToOne on both entitiesSide with @JoinColumn or @MapsId
One-to-many / many-to-one@OneToMany(mappedBy = "parent") and @ManyToOneUsually the many side with the foreign key
Many-to-many@ManyToMany with @JoinTableSide 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 mappedBy on 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() or hashCode().
  • Use DTOs or serialization annotations to prevent infinite JSON recursion.
  • Use orphanRemoval only when the child lifecycle belongs to the parent.
  • Add database foreign-key, unique, and join-table constraints.
Summary: bidirectional associations provide navigation in both directions, but the owning side alone controls relationship persistence. Keep both references synchronized in application code.