Orphan Removal

Removing a Child from the Collection

orphanRemoval = true means: if a child is removed from the parent's collection (or a one-to-one reference is set to null), it is an orphan and Hibernate deletes its row at flush time. You do not call delete yourself.

@Transactional
public void removeItem(Long orderId, Long itemId) {
    Order order = orderRepository.findById(orderId).orElseThrow();
    OrderItem item = order.getItems().stream()
            .filter(i -> i.getId().equals(itemId)).findFirst().orElseThrow();
    order.removeItem(item);                       // helper: items.remove(item); item.setOrder(null)
    order.recalculateTotal();
    // no save() and no delete() needed: flush at commit runs the DELETE and the UPDATE
}
delete from order_items where id=?
update orders set status=?,total=?,user_id=? where id=?

Simulating Cascade and Orphan Removal

Example

This small simulation does what Hibernate does: cascade the INSERTs to the children, and at flush compare the collection with the snapshot loaded from the database to find orphans.

import java.util.*;

public class Main {
    static long nextId = 1;
    static final List<String> sql = new ArrayList<>();

    public static void main(String[] args) {
        Order order = new Order();
        order.items.add(new Item("Phone case", 1));
        order.items.add(new Item("Charger", 2));
        order.items.add(new Item("Earphones", 1));

        persist(order);                                  // cascade = PERSIST
        sql.forEach(System.out::println);

        Set<Long> snapshot = new TreeSet<>();            // rows now in the DB
        order.items.forEach(i -> snapshot.add(i.id));

        sql.clear();
        order.items.removeIf(i -> i.name.equals("Charger"));
        flush(order, snapshot);                          // orphanRemoval = true
        System.out.println("-- after removing Charger from order.items:");
        sql.forEach(System.out::println);
    }

    static void persist(Order o) {
        o.id = nextId++;
        sql.add("insert into orders (id) values (" + o.id + ")");
        for (Item i : o.items) {
            i.id = nextId++;
            sql.add("insert into order_items (id,order_id,name,qty) values (" + i.id + "," + o.id + ",'" + i.name + "'," + i.qty + ")");
        }
    }

    static void flush(Order o, Set<Long> dbIds) {
        Set<Long> current = new TreeSet<>();
        o.items.forEach(i -> current.add(i.id));
        for (Long id : dbIds) {
            if (!current.contains(id)) sql.add("delete from order_items where id=" + id);
        }
    }
}

class Order { Long id; final List<Item> items = new ArrayList<>(); }

class Item {
    Long id;
    final String name;
    final int qty;
    Item(String name, int qty) { this.name = name; this.qty = qty; }
}

Output

insert into orders (id) values (1)
insert into order_items (id,order_id,name,qty) values (2,1,'Phone case',1)
insert into order_items (id,order_id,name,qty) values (3,1,'Charger',2)
insert into order_items (id,order_id,name,qty) values (4,1,'Earphones',1)
-- after removing Charger from order.items:
delete from order_items where id=3

REMOVE vs orphanRemoval

CascadeType.REMOVEorphanRemoval = true
Parent deletedchildren deletedchildren deleted
Child removed from collectionnothing happens (child keeps its row)child row deleted
Child moved to another parentFK updatedavoid: it is treated as an orphan and deleted

Note: Never replace an orphan-removal collection with a new list (order.setItems(new ArrayList<>())). Hibernate tracks the original collection object and throws HibernateException: A collection with cascade="all-delete-orphan" was no longer referenced by the owning entity instance. Use clear() and addAll() on the existing collection instead.