Вопрос 22 · Раздел 16

Что такое orphan removal

Orphan removal — механизм автоматического удаления связанных сущностей, когда они удаляются из коллекции или связь с ними разрывается. Это мощный инструмент для поддержания цело...

Версии по языкам: English Russian Ukrainian

Обзор

Orphan removal — механизм автоматического удаления связанных сущностей, когда они удаляются из коллекции или связь с ними разрывается. Это мощный инструмент для поддержания целостности данных в композициях.


🟢 Junior Level

Что такое orphan removal

Orphan removal — автоматически удаляет связанные сущности из БД, когда они удаляются из коллекции.

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

Order order = entityManager.find(Order.class, 1L);
order.getItems().remove(0);  // удалённый item автоматически удалится из БД!

Cascade vs orphanRemoval

// Cascade — propagates операции
@OneToMany(cascade = CascadeType.REMOVE)
// entityManager.remove(order) → remove для всех items

// orphanRemoval — удаляет orphan entities
@OneToMany(orphanRemoval = true)
// order.getItems().remove(item) → DELETE для item из БД

// Вместе — полный контроль
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
// persist, merge, remove + automatic orphan deletion

Пример

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

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

// Использование
Order order = entityManager.find(Order.class, 1L);
order.removeItem(order.getItems().get(0));
// item удалён из БД автоматически!

🟡 Middle Level

Cascade + orphanRemoval вместе

@OneToMany(mappedBy = "order", cascade = CascadeType.ALL, orphanRemoval = true)
private List<OrderItem> items;

order.getItems().add(newItem);   // persist через cascade
order.getItems().remove(oldItem);  // delete через orphanRemoval
order.getItems().clear();         // delete ALL через orphanRemoval

Когда orphanRemoval работает

✅ @OneToMany
✅ @OneToOne
❌ @ManyToOne — бессмысленно (orphanRemoval имеет смысл только для collections и one-to-one где родитель "владеет" ребёнком)
❌ @ManyToMany — бессмысленно (в Many-to-Many нет однозначного владельца)

Типичные ошибки

// ❌ orphanRemoval для shared entities
@ManyToOne(orphanRemoval = true)  // ❌ нельзя для @ManyToOne
private User user;

// ❌ orphanRemoval для @ManyToMany
@ManyToMany(orphanRemoval = true)  // ❌ нельзя для @ManyToMany
private List<Tag> tags;

// ❌ Без понимания разницы
@OneToMany(cascade = CascadeType.REMOVE)  // Только при удалении parent
private List<OrderItem> items;

@OneToMany(orphanRemoval = true)  // При удалении из коллекции
private List<OrderItem> items;

Пример с @OneToOne

@Entity
public class User {
    @OneToOne(mappedBy = "user", orphanRemoval = true, cascade = CascadeType.ALL)
    private UserProfile profile;
}

User user = entityManager.find(User.class, 1L);
user.setProfile(null);  // profile удалён из БД

🔴 Senior Level

Внутренняя реализация

orphanRemoval mechanism:
1. При flush: Hibernate проверяет все коллекции с orphanRemoval
2. Сравнивает текущее состояние с snapshot
3. Entities которые были в snapshot, но нет в текущем → orphan
4. Для каждого orphan → schedule DELETE
5. При flush → DELETE из БД

Важно: orphanRemoval срабатывает при flush, не сразу!
Flush происходит при: (1) commit транзакции, (2) явном вызове entityManager.flush(), (3) перед выполнением SQL-запроса.

Опасности

// ❌ clear() коллекции — удалит ВСЕ элементы. В легитимных случаях (полная замена содержимого) это именно то, что нужно. Опасно когда clear() вызван случайно из-за бага.
order.getItems().clear();  // все items удалены из БД!

// ✅ Правильно — осознанное удаление
order.getItems().remove(item);  // только один item

Комбинирование с @OnDelete

@Entity
public class Order {
    @OneToMany(mappedBy = "order", orphanRemoval = true)
    @org.hibernate.annotations.OnDelete(
        action = org.hibernate.annotations.OnDeleteAction.CASCADE
    )
    private List<OrderItem> items;
}

// Двойная защита:
// 1. orphanRemoval — на уровне JPA
// 2. ON DELETE CASCADE — на уровне БД

Production паттерны

// Pattern 1: Helper method
@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) {
        items.remove(item);
        item.setOrder(null);  // важно для консистентности
    }
}

// Pattern 2: Replace collection
Order order = entityManager.find(Order.class, 1L);
order.setItems(newItems);  // ❌ old items — orphans, удалены!

// ✅ Лучше — модифицировать коллекцию
order.getItems().clear();
order.getItems().addAll(newItems);

Best Practices

✅ orphanRemoval для composite children
✅ С cascade = ALL
✅ Helper methods для модификации коллекции
✅ Только для private ownership

❌ Для shared entities
❌ Без cascade
❌ Для @ManyToOne или @ManyToMany
❌ Замена всей коллекции без понимания последствий

🎯 Шпаргалка для интервью

Обязательно знать:

  • orphanRemoval — автоматически удаляет из БД сущности удалённые из коллекции
  • Cascade.REMOVE — только при удалении родителя, orphanRemoval — при удалении из коллекции
  • Работает только для @OneToMany и @OneToOne, НЕ для @ManyToOne/@ManyToMany
  • Срабатывает при flush, не сразу — важно для понимания тайминга
  • Всегда использовать с cascade = ALL для полного контроля
  • Helper methods обязательны: removeItem() удаляет из коллекции + устанавливает null

Частые уточняющие вопросы:

  • Cascade.REMOVE vs orphanRemoval? REMOVE — при entityManager.remove(parent), orphanRemoval — при parent.getChildren().remove(child)
  • Почему orphanRemoval срабатывает не сразу? Как и dirty checking, выполняется при flush — важно для транзакций
  • Что будет при clear() коллекции? ВСЕ элементы удалены из БД — в легитимных случаях это нужно, при баге — катастрофа
  • Можно ли комбинировать с @OnDelete? Да — двойная защита: orphanRemoval (JPA) + ON DELETE CASCADE (БД)

Красные флаги (НЕ говорить):

  • «orphanRemoval для @ManyToOne» — не поддерживается
  • «orphanRemoval для @ManyToMany» — нет однозначного владельца
  • «orphanRemoval без cascade» — orphanRemoval только для удаления, другие операции не propagates
  • «Заменяю всю коллекцию без понимания» — old items станут orphans и будут удалены

Связанные темы:

  • [[20. Как работают каскадные операции (Cascade)]]
  • [[21. Какие типы Cascade существуют]]
  • [[23. Как правильно использовать @OneToMany и @ManyToOne]]
  • [[24. В чём особенности bidirectional relationships]]