Программирование

Hibernate 6: ManyToOne с каскадным удалением в БД

Решение TransientObjectException в Hibernate 6 при односторонней связи @ManyToOne и ON DELETE CASCADE. Варианты: native DELETE + entityManager.clear(), @OnDelete, JPA-каскад. Примеры для Spring Boot.

Hibernate 6: Односторонняя связь Many-to-One с каскадным удалением в базе данных

У меня возникла проблема в Hibernate 6 при использовании односторонних связей @ManyToOne с каскадным удалением в базе данных.

Описание проблемы

В моей JPA-модели у меня есть односторонние связи @ManyToOne от дочерних сущностей к родительским:

java
public class Child {
 @ManyToOne(optional = false)
 @JoinColumn(name = "parent")
 private Parent parent
}

Я настроил каскадное удаление в базе данных для повышения эффективности, поэтому мне нужно удалять только самую верхнюю сущность, когда я хочу удалить связанные сущности. Этот подход хорошо работал в предыдущих версиях Hibernate.

Однако в Hibernate 6, если я загрузил сущность Child, а затем пытаюсь удалить сущность Parent, я получаю следующее исключение:

org.hibernate.TransientObjectException: persistent instance references an unsaved transient instance of 'Parent'

Исключение возникает через CascadingAction.CHECK_ON_FLUSH и, похоже, проверяет, что загруженные сущности не имеют висящих ссылок. Поскольку я не удаляю сущности через JPA, а использую каскадное удаление в базе данных, эти ссылки будут естественно существовать.

Вопрос

Есть ли способ:

  1. Отключить эту проверку в Hibernate 6?
  2. Пометить эти связи как каскадное удаление в базе данных, чтобы Hibernate знал, что он может безопасно удалять сущности?

Я предпочитаю не повторять каскадное удаление в базе данных в JPA, так как это значительно медленнее.

Примечание: Я использую Spring Boot.

hibernate 6: при односторонней связи ManyToOne отключить встроенную проверку нельзя — она следует из спецификации JPA и приводит к org.hibernate.TransientObjectException при смешивании DB‑level ON DELETE CASCADE и загруженных дочерних сущностей. Решения: синхронизировать persistence context с действиями СУБД (detach/clear или native DELETE + entityManager.clear() / @Modifying(clearAutomatically = true)), либо перейти на JPA‑каскад/обратную коллекцию (CascadeType.REMOVE / @PreRemove), либо использовать Hibernate‑специфику (@OnDelete, DeleteEventListener) с пониманием компромиссов по производительности и согласованности кэша.


Содержание


Проблема: TransientObjectException при односторонней Many-to-One в Hibernate 6

Коротко: вы используете одностороннюю связь @ManyToOne (Child → Parent) и включили ON DELETE CASCADE в базе, чтобы не выполнять удаление дочерних записей через JPA. Если до удаления Parent вы загрузили хотя бы один Child в persistence context, при удалении Parent Hibernate 6 кидает:

org.hibernate.TransientObjectException: persistent instance references an unsaved transient instance of 'Parent'

Это происходит потому, что в Hibernate 6 появилась более строгая проверка ссылок при flush: проверяется каждый персистентный объект, а не только “грязные”. Официальное обсуждение разъясняет этот режим и перечисляет обходы: detach/clear, native DELETE + clear, добавление JPA-каскада и т.д. — см. подробности на форуме Hibernate: https://discourse.hibernate.org/t/transientobjectexception-when-mixing-entitymanager-delete-with-delete-statements-in-6-6-x/10621.


Почему Hibernate 6 проверяет ссылки на flush (JPA 3.1)

Коротко: это не “баг” Hibernate, а соответствие спецификации. JPA (Jakarta Persistence 3.1 §3.2.4) требует бросать исключение, если сущность X ссылается на новую или удалённую сущность Y и связь не имеет cascade PERSIST/ALL — иначе состояние может стать несогласованным. Hibernate в версии 6 усилил проверку: она выполняется для всех персистентных сущностей, поэтому случаи, которые раньше проходили, теперь приводят к исключению. Обсуждение и цитаты — https://discourse.hibernate.org/t/transientobjectexception-when-mixing-entitymanager-delete-with-delete-statements-in-6-6-x/10621.

Что это даёт и почему это важно? Hibernate защищает вас от ситуации, когда в памяти есть объекты, ссылающиеся на сущности, которые вы удалили на уровне БД — это ведёт к несоответствию первого уровня кэша (session) и реального состояния БД. Но цена — смешивание DB-level cascades и загруженных сущностей становится хрупким; об этом также пишут практики и эксперты (Vlad Mihalcea, Thorben Janssen) — https://vladmihalcea.com/cascade-delete-hibernate-events/, https://thorben-janssen.com/avoid-cascadetype-delete-many-assocations/.


Практические варианты решения и их компромиссы

Ниже — проверенные подходы. Выберите тот, который подходит по требованиям к производительности, сложности и согласованности.

  1. Нельзя отключить проверку глобально
  1. Полностью JPA-каскад (обратная коллекция + CascadeType.REMOVE / orphanRemoval)
  • Что: добавить @OneToMany на Parent и cascade = CascadeType.REMOVE (или orphanRemoval = true) — тогда при entityManager.remove(parent) Hibernate удалит детей сам.
  • Плюсы: согласованность persistence context, работает в транзакции.
  • Минусы: при больших коллекциях Hibernate может выполнить много отдельных DELETE, медленнее, чем DB-level CASCADE. См. обсуждение рисков CascadeType.REMOVE: https://thorben-janssen.com/avoid-cascadetype-delete-many-assocations/.
  1. DB-level ON DELETE CASCADE + синхронизация persistence context (рекомендуемый компромисс)
  • Что: оставить каскад в БД, но перед удалением Parent убедиться, что в сессии нет загруженных Child, которые будут ссылаться на удаляемый Parent. Два простых способа:
  • Выполнить native DELETE и затем вызвать entityManager.clear() (evict всех сущностей).
  • Или использовать Spring Data JPA @Modifying(clearAutomatically = true) на native/JPQL запросе — persistence context будет очищен автоматически.
  • Плюсы: быстро (DB делает работу), просто реализовать.
  • Минусы: clear() удаляет все объекты из контекстa (в длинной транзакции это может быть нежелательно). Лучше детачить только затронутые children, если возможно.
  • Примеры кода — ниже; также обсуждение практики в issue Spring Data: https://github.com/spring-projects/spring-data-jpa/issues/2281.
  1. Detach только затронутых Child перед удалением
  • Что: найти загруженные дочерние объекты и entityManager.detach(child) для каждого (или entityManager.clear()).
  • Плюсы: не трогает DB, не приводит к множественным DELETE от Hibernate.
  • Минусы: требует знать/находить загруженные объекты; возможно лишняя работа, если детей много.
  1. Hibernate @OnDelete(action = OnDeleteAction.CASCADE)
  1. Использовать Hibernate DeleteEventListener / интерцепторы
  • Что: зарегистрировать слушателя событий удаления, который либо выполнит очистку сессии, либо выполнит дополнительные операции.
  • Плюсы: автоматизация на уровне Hibernate, можно убрать дублирование кода.
  • Минусы: более сложная настройка; требует понимания жизненного цикла Hibernate и тестирования. См. идеи: https://vladmihalcea.com/cascade-delete-hibernate-events/.
  1. JPQL bulk update / delete — осторожно
  • Bulk-операции (JPQL UPDATE/DELETE) обходят entity lifecycle и не вызывают каскады; они меняют БД напрямую и не синхронизируют объекты в persistence context. Если вы используете bulk‑операции, вызывайте entityManager.clear() или регулярно синхронизируйте контекст.

Короткая шпаргалка по выбору:

  • Нужна максимальная скорость при удалении большого количества детей → DB-level CASCADE + native DELETE + затем clear() / @Modifying(clearAutomatically=true).
  • Нужна безопасность и согласованность в приложении (простая модель) → JPA Cascade (OneToMany + CascadeType.REMOVE).
  • Хотите меньше кода и DDL → @OnDelete, но всегда чистите persistence context после удаления.

Реализации и примеры кода (Spring Boot)

Ниже — готовые фрагменты для типичных сценариев.

  1. Native DELETE + entityManager.clear()
java
@Service
public class ParentService {
 @PersistenceContext
 private EntityManager em;

 @Transactional
 public void deleteParentDbCascade(Long id) {
 em.flush(); // убедиться, что все pending changes отправлены
 em.createNativeQuery("DELETE FROM parent WHERE id = :id")
 .setParameter("id", id)
 .executeUpdate();
 em.clear(); // важно — убрать из persistence context загруженные Child
 }
}
  1. Spring Data JPA: native DELETE с автоматической очисткой контекста
java
public interface ParentRepository extends JpaRepository<Parent, Long> {
 @Modifying(clearAutomatically = true)
 @Query(value = "DELETE FROM parent WHERE id = :id", nativeQuery = true)
 void deleteByIdNative(@Param("id") Long id);
}

Вызовите этот метод внутри @Transactional; clearAutomatically = true поможет избежать устаревших объектов в контексте (см. обсуждение практик: https://github.com/spring-projects/spring-data-jpa/issues/2281).

  1. Detach загруженных детей перед удалением
java
List<Child> loaded = em.createQuery(
 "select c from Child c where c.parent.id = :id", Child.class)
 .setParameter("id", parentId)
 .getResultList();

loaded.forEach(em::detach); // удалить из persistence context
em.createNativeQuery("DELETE FROM parent WHERE id = :id")
 .setParameter("id", parentId)
 .executeUpdate();
  1. @OnDelete (Hibernate‑specific) — генерирует ON DELETE CASCADE (DDL)
java
import org.hibernate.annotations.OnDelete;
import org.hibernate.annotations.OnDeleteAction;

@ManyToOne(optional = false)
@JoinColumn(name = "parent_id", nullable = false)
@OnDelete(action = OnDeleteAction.CASCADE)
private Parent parent;

Помните: @OnDelete только меняет поведение на уровне БД; при наличии загруженных Child вам всё равно нужно очищать/детачить сессию.

  1. Когда у вас есть двунаправленная связь — @PreRemove для очистки ссылок (только если вы хотите не удалять детей)
java
@Entity
public class Parent {
 @OneToMany(mappedBy = "parent")
 private List<Child> children = new ArrayList<>();

 @PreRemove
 public void preRemove() {
 for (Child c : children) {
 c.setParent(null); // разорвать связь в памяти
 }
 }
}

Внимание: этот способ разрывает связь и предотвратит DB‑cascade, поэтому используйте его, если хотите сохранить детей (и не хотите DB удалить их).

  1. DeleteEventListener (для опытных)
  • Реализуйте listener, зарегистрируйте в Hibernate, чтобы автоматически вычищать/детачить связанные сущности перед/после удаления. Идея описана у Vlad Mihalcea: https://vladmihalcea.com/cascade-delete-hibernate-events/.

Рекомендации для production

  • Если ваша цель — максимальная скорость удаления больших деревьев, оставьте DB ON DELETE CASCADE, но после выполнения native DELETE обязательно синхронизируйте persistence context: либо entityManager.clear(), либо детачьте только те сущности, которые были загружены. Это самый практичный компромисс.
  • Если вы не хотите глобально терять контекст (clear() сильный), детачьте только конкретные дочерние объекты. Это чуть сложнее, но безопаснее в длинных транзакциях.
  • Для простых моделей с небольшими коллекциями используйте JPA CascadeType.REMOVE — он безопасен, предсказуем, но может быть медленнее.
  • Не пытайтесь «отключить» проверку Hibernate 6 — опции нет. Вместо этого выберите один из описанных обходов и документируйте поведение в команде.
  • Тестируйте: интеграционные тесты должны покрывать сценарии, где дети загружены в сессии, и удаление Parent происходит через DB-level cascades. Это ловушка, которую легко пропустить.

Источники

  1. https://discourse.hibernate.org/t/transientobjectexception-when-mixing-entitymanager-delete-with-delete-statements-in-6-6-x/10621
  2. https://discourse.hibernate.org/t/manytoone-without-cascade-remove/8811
  3. https://vladmihalcea.com/cascade-delete-hibernate-events/
  4. https://stackoverflow.com/questions/79811246/hibernate-6-unidirectional-many-to-one-relationship-and-database-cascading-delet
  5. https://www.baeldung.com/hibernate-unsaved-transient-instance-error
  6. https://github.com/spring-projects/spring-data-jpa/issues/2281
  7. https://thorben-janssen.com/avoid-cascadetype-delete-many-assocations/
  8. https://stackoverflow.com/questions/40286906/jpa-and-hibernate-cascade-delete-onetomany-does-not-work
  9. https://stackoverflow.com/questions/19626535/how-to-cascade-delete-entities-with-unidirectional-manytoone-relationship-with
  10. https://discourse.hibernate.org/t/combination-of-onremove-and-ordinary-cascading/8145

Заключение

Hibernate 6 следует JPA 3.1 и не позволяет глобально отключить проверку, вызывающую org.hibernate.TransientObjectException при смешивании DB-level ON DELETE CASCADE и загруженных сущностей; практичный путь — либо синхронизировать persistence context (detach/clear или native DELETE + clear/@Modifying(clearAutomatically = true)), либо перейти на JPA-каскад/обратную коллекцию, либо применять Hibernate‑специфику с явной очисткой кэша — выбор зависит от требований к производительности и сложности в вашем приложении.

Авторы
Проверено модерацией
Модерация
Hibernate 6: ManyToOne с каскадным удалением в БД