Bidirectional Helper Methods

Keep Both Sides in Sync

Hibernate only reads the owning side when saving, but your code reads both sides in memory. If you update only one, the object graph is inconsistent until the next reload. Add helper methods on the parent and use only them.

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

public void removeItem(OrderItem item) {
    items.remove(item);
    item.setOrder(null);
}

// expose a read-only view so nobody bypasses the helpers
public List<OrderItem> getItems() {
    return Collections.unmodifiableList(items);
}

See the Difference

Example

Plain Java objects behave exactly like entities in memory. Without the helper, the item does not know its order, so Hibernate would save order_id = NULL.

import java.util.*;

public class Main {
    public static void main(String[] args) {
        Order o1 = new Order(101);
        OrderItem pen = new OrderItem("Pen", 2);
        o1.items.add(pen);                     // forgot pen.order = o1
        System.out.println("Without helper:");
        System.out.println("  order 101 items = " + o1.items.size());
        System.out.println("  pen.order       = " + (pen.order == null ? "null -> order_id saved as NULL!" : pen.order.id));

        Order o2 = new Order(102);
        OrderItem book = new OrderItem("Book", 1);
        OrderItem bag = new OrderItem("Bag", 1);
        o2.addItem(book);
        o2.addItem(bag);
        System.out.println("With addItem():");
        System.out.println("  order 102 items = " + o2.items.size());
        System.out.println("  book.order      = " + book.order.id);

        o2.removeItem(bag);
        System.out.println("After removeItem(bag):");
        System.out.println("  order 102 items = " + o2.items.size());
        System.out.println("  bag.order       = " + bag.order);
    }
}

class Order {
    final long id;
    final List<OrderItem> items = new ArrayList<>();
    Order(long id) { this.id = id; }
    void addItem(OrderItem item)    { items.add(item); item.order = this; }
    void removeItem(OrderItem item) { items.remove(item); item.order = null; }
}

class OrderItem {
    final String product;
    final int qty;
    Order order;
    OrderItem(String product, int qty) { this.product = product; this.qty = qty; }
}

Output

Without helper:
  order 101 items = 1
  pen.order       = null -> order_id saved as NULL!
With addItem():
  order 102 items = 2
  book.order      = 102
After removeItem(bag):
  order 102 items = 1
  bag.order       = null

Avoid toString and JSON Loops

Note: A bidirectional pair points both ways, so a generated toString() (Lombok @Data) or Jackson serialization of an entity goes Order → items → order → items ... forever and ends in StackOverflowError. Exclude the back-reference from toString, and return DTOs from controllers instead of entities.

Practice

Exercise: Write addProduct and removeProduct helpers for the bidirectional Category.products / Product.category relationship.

Show answer
public void addProduct(Product product) {
    products.add(product);
    product.setCategory(this);
}

public void removeProduct(Product product) {
    products.remove(product);
    product.setCategory(null);   // only if category is optional; otherwise move it to another category
}