Question 24 · Section 16

What Are the Peculiarities of Bidirectional Relationships

Bidirectional relationships allow navigation between entities in both directions. They require manual synchronization of both sides and correct use of mappedBy.

Language versions: English Russian Ukrainian

Overview

Bidirectional relationships allow navigation between entities in both directions. They require manual synchronization of both sides and correct use of mappedBy.


Junior Level

What is a Bidirectional Relationship

This is a relationship where both sides know about each other.

// Order knows about User
@Entity
public class Order {
    @ManyToOne
    private User user;
}

// User knows about Orders
@Entity
public class User {
    @OneToMany(mappedBy = "user")
    private List<Order> orders;
}

Owner Side and Inverse Side

Owner side - the side with FK (Order with @ManyToOne).

Inverse side - the side with mappedBy (User with @OneToMany(mappedBy = "user")).

// Owner - contains @JoinColumn
@ManyToOne
@JoinColumn(name = "user_id")
private User user;

// Inverse - contains mappedBy
@OneToMany(mappedBy = "user")
private List<Order> orders;

Middle Level

Synchronization is Mandatory

@Entity
public class User {
    @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true)
    private List<Order> orders = new ArrayList<>();

    // Helper methods - mandatory!
    public void addOrder(Order order) {
        orders.add(order);
        order.setUser(this);  // MANDATORY!
    }

    public void removeOrder(Order order) {
        orders.remove(order);
        order.setUser(null);  // MANDATORY!
    }
}

Why Desynchronization is a Problem

// Desynchronized
User user = new User();
Order order = new Order();
user.getOrders().add(order);

// order.getUser() == null -> FK not set!
entityManager.persist(user);  // With cascade=ALL order saved,
// but order.getUser() = null -> FK column = NULL -> constraint violation or extra UPDATE

// Synchronized via helper
User user = new User();
Order order = new Order();
user.addOrder(order);  // setter + setUser

// order.getUser() == user -> FK set
entityManager.persist(user);  // cascade -> order saved

Common Mistakes

// Desynchronization
user.getOrders().add(order);
// order.user == null -> FK not set

// mappedBy on owner side
@ManyToOne(mappedBy = "orders")  // mappedBy only on inverse
private User user;

// Without helper methods
order.setUser(user);
// user.orders doesn't contain order -> inconsistency

Senior Level

equals/hashCode for Bidirectional

// Use business key, NOT ID!
@Entity
public class User {
    @Column(unique = true, nullable = false)
    private String email;  // business key

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof User)) return false;
        User user = (User) o;
        return email != null && email.equals(user.email);
    }

    @Override
    public int hashCode() {
        return email != null ? email.hashCode() : 0;
    }
}

Why Not ID for equals/hashCode

// ID for equals/hashCode - problems
@Entity
public class User {
    @Id
    @GeneratedValue
    private Long id;

    @Override
    public boolean equals(Object o) {
        if (!(o instanceof User)) return false;
        return id != null && id.equals(((User) o).id);
    }
}

// Problems:
// 1. Transient entity (id=null) != Transient entity (id=null)
// 2. HashSet loses entity after persist (id changes)
// 3. Cannot add to HashSet before persist

If no natural/business key: use UUID, assigned at object construction.

public class User {
    private final UUID uuid = UUID.randomUUID();
    // equals/hashCode by uuid
}

Business Key Pattern

@Entity
public class OrderItem {
    @Id
    @GeneratedValue
    private Long id;

    @Column(nullable = false)
    private String sku;  // business key

    @ManyToOne
    private Order order;

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof OrderItem)) return false;
        OrderItem that = (OrderItem) o;
        return sku != null && sku.equals(that.sku);
    }

    @Override
    public int hashCode() {
        return sku != null ? sku.hashCode() : 0;
    }
}

Advanced Synchronization

@Entity
public class Order {
    @OneToMany(mappedBy = "order", cascade = CascadeType.ALL, orphanRemoval = true)
    private List<OrderItem> items = new ArrayList<>();

    public void addItem(OrderItem item) {
        items.add(item);
        item.setOrder(this);
    }

    public void removeItem(OrderItem item) {
        if (items.remove(item)) {
            item.setOrder(null);  // important for consistency
        }
    }

    // For replace
    public void replaceItems(List<OrderItem> newItems) {
        items.clear();  // orphanRemoval deletes old
        for (OrderItem item : newItems) {
            addItem(item);
        }
    }
}

Performance Considerations

Bidirectional overhead:
- Helper methods (minimal)
- Memory for both references
- Synchronization on change

Benefits:
- Navigation in both directions
- Cascade with orphanRemoval
- Complete domain model

Best Practices

Helper methods for synchronization
Business key for equals/hashCode
mappedBy on inverse side
Synchronize both sides
Initialize collections

ID for equals/hashCode
Without helper methods
mappedBy on owner side
Desynchronization
Direct collection modification

Interview Cheat Sheet

Must know:

  • Bidirectional = navigation in both directions, requires synchronization of both sides
  • Owner side - with FK (@ManyToOne), Inverse side - with mappedBy (@OneToMany)
  • Helper methods MANDATORY: addOrder/addItem synchronize both sides
  • equals/hashCode by business key (email, SKU), NOT by ID - ID changes on persist
  • For equals/hashCode without business key - use UUID at object construction
  • mappedBy only on inverse side - doesn’t exist on owner side

Frequent follow-up questions:

  • Why is ID bad for equals/hashCode? Transient entity (id=null) != Transient entity, HashSet loses after persist
  • What if desynchronized? FK not set or NULL - constraint violation or extra UPDATE
  • What if no business key? UUID on object creation - stable identifier before persist
  • Performance overhead of bidirectional? Minimal: helper methods + memory for references, benefits outweigh costs

Red flags (DO NOT say):

  • “ID for equals/hashCode” - HashSet/HashMap problems, entity loss
  • “Without helper methods” - desynchronization, FK not set
  • “Direct collection modification” - bypass helper methods -> inconsistency
  • “mappedBy on owner side” - doesn’t exist, only on inverse

Related topics:

  • [[23. How to Properly Use @OneToMany and @ManyToOne]]
  • [[22. What is Orphan Removal]]
  • [[25. How to Avoid Infinite Recursion When Serializing Entities]]
  • [[20. How Do Cascade Operations Work]]