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 от дочерних сущностей к родительским:
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, а использую каскадное удаление в базе данных, эти ссылки будут естественно существовать.
Вопрос
Есть ли способ:
- Отключить эту проверку в Hibernate 6?
- Пометить эти связи как каскадное удаление в базе данных, чтобы 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
- Почему Hibernate 6 так проверяет (JPA 3.1 и CHECK_ON_FLUSH)
- Практические варианты решения и их компромиссы
- Реализации и примеры кода (Spring Boot)
- Рекомендации для production
- Источники
- Заключение
Проблема: 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/.
Практические варианты решения и их компромиссы
Ниже — проверенные подходы. Выберите тот, который подходит по требованиям к производительности, сложности и согласованности.
- Нельзя отключить проверку глобально
- Короткий ответ: конфигурационной опции “выключить check” в Hibernate 6 нет. Как написал разработчик/форум, “There is no API to ‘ignore’ the check” — https://discourse.hibernate.org/t/transientobjectexception-when-mixing-entitymanager-delete-with-delete-statements-in-6-6-x/10621. Значит, нужно работать обходными путями.
- Полностью 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/.
- 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.
- Detach только затронутых Child перед удалением
- Что: найти загруженные дочерние объекты и
entityManager.detach(child)для каждого (илиentityManager.clear()). - Плюсы: не трогает DB, не приводит к множественным DELETE от Hibernate.
- Минусы: требует знать/находить загруженные объекты; возможно лишняя работа, если детей много.
- Hibernate @OnDelete(action = OnDeleteAction.CASCADE)
- Что: аннотация Hibernate, генерирует FK с ON DELETE CASCADE (DDL).
- Плюсы: удобно при генерации схемы, быстрее на уровне БД.
- Минусы: не синхронизирует первый уровень кэша — после удаления в БД объекты в session останутся, и без очистки вы снова получите исключения. См. обсуждение и рекомендации: https://thorben-janssen.com/avoid-cascadetype-delete-many-assocations/, https://vladmihalcea.com/cascade-delete-hibernate-events/.
- Использовать Hibernate DeleteEventListener / интерцепторы
- Что: зарегистрировать слушателя событий удаления, который либо выполнит очистку сессии, либо выполнит дополнительные операции.
- Плюсы: автоматизация на уровне Hibernate, можно убрать дублирование кода.
- Минусы: более сложная настройка; требует понимания жизненного цикла Hibernate и тестирования. См. идеи: https://vladmihalcea.com/cascade-delete-hibernate-events/.
- 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)
Ниже — готовые фрагменты для типичных сценариев.
- Native DELETE + entityManager.clear()
@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
}
}
- Spring Data JPA: native DELETE с автоматической очисткой контекста
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).
- Detach загруженных детей перед удалением
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();
- @OnDelete (Hibernate‑specific) — генерирует ON DELETE CASCADE (DDL)
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 вам всё равно нужно очищать/детачить сессию.
- Когда у вас есть двунаправленная связь — @PreRemove для очистки ссылок (только если вы хотите не удалять детей)
@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 удалить их).
- 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. Это ловушка, которую легко пропустить.
Источники
- https://discourse.hibernate.org/t/transientobjectexception-when-mixing-entitymanager-delete-with-delete-statements-in-6-6-x/10621
- https://discourse.hibernate.org/t/manytoone-without-cascade-remove/8811
- https://vladmihalcea.com/cascade-delete-hibernate-events/
- https://stackoverflow.com/questions/79811246/hibernate-6-unidirectional-many-to-one-relationship-and-database-cascading-delet
- https://www.baeldung.com/hibernate-unsaved-transient-instance-error
- https://github.com/spring-projects/spring-data-jpa/issues/2281
- https://thorben-janssen.com/avoid-cascadetype-delete-many-assocations/
- https://stackoverflow.com/questions/40286906/jpa-and-hibernate-cascade-delete-onetomany-does-not-work
- https://stackoverflow.com/questions/19626535/how-to-cascade-delete-entities-with-unidirectional-manytoone-relationship-with
- 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‑специфику с явной очисткой кэша — выбор зависит от требований к производительности и сложности в вашем приложении.