diff --git a/hibernate-core/src/main/java/org/hibernate/annotations/OnDelete.java b/hibernate-core/src/main/java/org/hibernate/annotations/OnDelete.java
index 8a748bdc5216..791200e790a9 100644
--- a/hibernate-core/src/main/java/org/hibernate/annotations/OnDelete.java
+++ b/hibernate-core/src/main/java/org/hibernate/annotations/OnDelete.java
@@ -17,10 +17,33 @@
/**
* Specifies an {@code on delete} action for a foreign key constraint.
* The most common usage is {@code @OnDelete(action = CASCADE)}.
+ *
+ * @ManyToOne
+ * @OnDelete(action = CASCADE)
+ * Parent parent;
+ *
* Note that this results in an {@code on delete cascade} clause in
* the DDL definition of the foreign key. It's completely different
* to {@link jakarta.persistence.CascadeType#REMOVE}.
*
+ * In fact, {@code @OnDelete} may be combined with {@code cascade=REMOVE}.
+ *
+ * @ManyToOne(cascade = REMOVE)
+ * @OnDelete(action = CASCADE)
+ * Parent parent;
+ *
+ *
+ * - If {@code @OnDelete(action = CASCADE)} is used in conjunction
+ * with {@code cascade=REMOVE}, then associated entities are fetched
+ * from the database, marked deleted in the persistence context,
+ * and evicted from the second-level cache.
+ *
- If {@code @OnDelete(action = CASCADE)} is used on its own,
+ * without {@code cascade=REMOVE}, then associated
+ * entities are not fetched from the database, are not marked
+ * deleted in the persistence context, and are not automatically
+ * evicted from the second-level cache.
+ *
+ *
* Like database triggers, {@code on delete} actions can cause state
* held in memory to lose synchronization with the database.
*
diff --git a/hibernate-core/src/main/java/org/hibernate/engine/internal/Cascade.java b/hibernate-core/src/main/java/org/hibernate/engine/internal/Cascade.java
index 1250ba154721..c5cde212c9bf 100644
--- a/hibernate-core/src/main/java/org/hibernate/engine/internal/Cascade.java
+++ b/hibernate-core/src/main/java/org/hibernate/engine/internal/Cascade.java
@@ -14,6 +14,7 @@
import org.hibernate.HibernateException;
import org.hibernate.bytecode.enhance.spi.interceptor.LazyAttributeLoadingInterceptor;
+import org.hibernate.annotations.OnDeleteAction;
import org.hibernate.collection.spi.PersistentCollection;
import org.hibernate.engine.spi.CascadeStyle;
import org.hibernate.engine.spi.CascadingAction;
@@ -137,6 +138,7 @@ public static void cascade(
final boolean isUninitializedProperty =
hasUninitializedLazyProperties &&
!persister.getBytecodeEnhancementMetadata().isAttributeLoaded( parent, propertyName );
+ final boolean isCascadeDeleteEnabled = cascadeDeleteEnabled( action, persister, i );
if ( style.doCascade( action ) ) {
final Object child;
@@ -200,7 +202,7 @@ else if ( action.performOnLazyProperty() && type instanceof EntityType ) {
style,
propertyName,
anything,
- false
+ isCascadeDeleteEnabled
);
}
else {
@@ -215,7 +217,7 @@ else if ( action.performOnLazyProperty() && type instanceof EntityType ) {
type,
style,
propertyName,
- false
+ isCascadeDeleteEnabled
);
}
}
@@ -471,8 +473,8 @@ private static void cascadeComponent(
componentPropertyStyle,
subPropertyName,
anything,
- false
- );
+ cascadeDeleteEnabled( action, componentType, i )
+ );
}
}
}
@@ -542,7 +544,7 @@ private static void cascadeCollection(
style,
elemType,
anything,
- persister.isCascadeDeleteEnabled()
+ cascadeDeleteEnabled( action, persister )
);
}
}
@@ -678,4 +680,19 @@ private static void deleteOrphans(EventSource eventSource, String entityName, Pe
}
}
}
+
+ private static boolean cascadeDeleteEnabled(CascadingAction action, CollectionPersister persister) {
+ return action.directionAffectedByCascadeDelete() == ForeignKeyDirection.FROM_PARENT
+ && persister.isCascadeDeleteEnabled();
+ }
+
+ private static boolean cascadeDeleteEnabled(CascadingAction action, EntityPersister persister, int i) {
+ return action.directionAffectedByCascadeDelete() == ForeignKeyDirection.TO_PARENT
+ && persister.getEntityMetamodel().getPropertyOnDeleteActions()[i] == OnDeleteAction.CASCADE;
+ }
+
+ private static boolean cascadeDeleteEnabled(CascadingAction action, CompositeType componentType, int i) {
+ return action.directionAffectedByCascadeDelete() == ForeignKeyDirection.TO_PARENT
+ && componentType.getOnDeleteAction( i ) == OnDeleteAction.CASCADE;
+ }
}
diff --git a/hibernate-core/src/main/java/org/hibernate/engine/spi/CascadingAction.java b/hibernate-core/src/main/java/org/hibernate/engine/spi/CascadingAction.java
index 36cbc840fdf9..22a2de1304f4 100644
--- a/hibernate-core/src/main/java/org/hibernate/engine/spi/CascadingAction.java
+++ b/hibernate-core/src/main/java/org/hibernate/engine/spi/CascadingAction.java
@@ -7,16 +7,22 @@
package org.hibernate.engine.spi;
import java.util.Iterator;
+import java.util.List;
+import org.checkerframework.checker.nullness.qual.Nullable;
import org.hibernate.HibernateException;
+import org.hibernate.Incubating;
import org.hibernate.event.spi.EventSource;
import org.hibernate.persister.entity.EntityPersister;
import org.hibernate.type.CollectionType;
+import org.hibernate.type.ForeignKeyDirection;
import org.hibernate.type.Type;
/**
* A session action that may be cascaded from parent entity to its children
*
+ * @param The type of some context propagated with the cascading action
+ *
* @author Gavin King
* @author Steve Ebersole
*/
@@ -27,10 +33,9 @@ public interface CascadingAction {
*
* @param session The session within which the cascade is occurring.
* @param child The child to which cascading should be performed.
- * @param entityName The child's entity name
- * @param anything Anything ;) Typically some form of cascade-local cache
- * which is specific to each CascadingAction type
- * @param isCascadeDeleteEnabled Are cascading deletes enabled.
+ * @param anything Some context specific to the kind of {@link CascadingAction}
+ * @param isCascadeDeleteEnabled Whether the foreign key is declared with
+ * {@link org.hibernate.annotations.OnDeleteAction#CASCADE on delete cascade}.
*/
void cascade(
EventSource session,
@@ -92,4 +97,18 @@ default void noCascade(EventSource session, Object parent, EntityPersister persi
* Should this action be performed (or noCascade consulted) in the case of lazy properties.
*/
boolean performOnLazyProperty();
+
+ /**
+ * The cascade direction in which we care whether the foreign key is declared with
+ * {@link org.hibernate.annotations.OnDeleteAction#CASCADE on delete cascade}.
+ *
+ * @apiNote This allows us to reuse the long-existing boolean parameter of
+ * {@link #cascade(EventSource, Object, String, Object, boolean)}
+ * for multiple purposes.
+ *
+ */
+ @Incubating @Nullable
+ default ForeignKeyDirection directionAffectedByCascadeDelete(){
+ return null;
+ }
}
diff --git a/hibernate-core/src/main/java/org/hibernate/engine/spi/CascadingActions.java b/hibernate-core/src/main/java/org/hibernate/engine/spi/CascadingActions.java
index f58cda812fbf..a49da298f4c5 100644
--- a/hibernate-core/src/main/java/org/hibernate/engine/spi/CascadingActions.java
+++ b/hibernate-core/src/main/java/org/hibernate/engine/spi/CascadingActions.java
@@ -6,6 +6,7 @@
*/
package org.hibernate.engine.spi;
+import org.checkerframework.checker.nullness.qual.Nullable;
import org.hibernate.HibernateException;
import org.hibernate.Internal;
import org.hibernate.LockMode;
@@ -20,6 +21,7 @@
import org.hibernate.event.spi.RefreshContext;
import org.hibernate.internal.CoreMessageLogger;
import org.hibernate.type.CollectionType;
+import org.hibernate.type.ForeignKeyDirection;
import org.jboss.logging.Logger;
import java.util.Iterator;
@@ -73,6 +75,11 @@ public boolean deleteOrphans() {
return true;
}
+ @Override
+ public ForeignKeyDirection directionAffectedByCascadeDelete() {
+ return ForeignKeyDirection.FROM_PARENT;
+ }
+
@Override
public String toString() {
return "ACTION_DELETE";
@@ -378,7 +385,7 @@ public void cascade(
Void context,
boolean isCascadeDeleteEnabled)
throws HibernateException {
- if ( child != null && isChildTransient( session, child, entityName ) ) {
+ if ( child != null && isChildTransient( session, child, entityName, isCascadeDeleteEnabled ) ) {
throw new TransientObjectException( "persistent instance references an unsaved transient instance of '"
+ entityName + "' (save the transient instance before flushing)" );
//TODO: should be TransientPropertyValueException
@@ -419,13 +426,18 @@ public boolean performOnLazyProperty() {
return false;
}
+ @Override
+ public ForeignKeyDirection directionAffectedByCascadeDelete() {
+ return ForeignKeyDirection.TO_PARENT;
+ }
+
@Override
public String toString() {
return "ACTION_CHECK_ON_FLUSH";
}
};
- private static boolean isChildTransient(EventSource session, Object child, String entityName) {
+ private static boolean isChildTransient(EventSource session, Object child, String entityName, boolean isCascadeDeleteEnabled) {
if ( isHibernateProxy( child ) ) {
// a proxy is always non-transient
// and ForeignKeys.isTransient()
@@ -440,7 +452,11 @@ private static boolean isChildTransient(EventSource session, Object child, Strin
// we are good, even if it's not yet
// inserted, since ordering problems
// are detected and handled elsewhere
- return entry.getStatus().isDeletedOrGone();
+ return entry.getStatus().isDeletedOrGone()
+ // if the foreign key is 'on delete cascade'
+ // we don't have to throw because the database
+ // will delete the parent for us
+ && !isCascadeDeleteEnabled;
}
else {
// TODO: check if it is a merged entity which has not yet been flushed
@@ -495,6 +511,11 @@ public abstract static class BaseCascadingAction implements CascadingAction getSelectables() {
public java.util.List getColumns() {
return value.getColumns();
}
-
+
public String getName() {
return name;
}
-
+
public boolean isComposite() {
return value instanceof Component;
}
@@ -137,6 +138,10 @@ public void resetOptional(boolean optional) {
}
}
+ public OnDeleteAction getOnDeleteAction() {
+ return value instanceof ToOne ? ( (ToOne) value ).getOnDeleteAction() : null;
+ }
+
/**
* @deprecated this method is no longer used
*/
@@ -158,7 +163,7 @@ else if ( type instanceof CollectionType ) {
return getCollectionCascadeStyle( collection.getElement().getType(), cascade );
}
else {
- return getCascadeStyle( cascade );
+ return getCascadeStyle( cascade );
}
}
@@ -192,7 +197,7 @@ else if ( elementType instanceof ComponentType ) {
return getCascadeStyle( cascade );
}
}
-
+
private static CascadeStyle getCascadeStyle(String cascade) {
if ( cascade==null || cascade.equals("none") ) {
return CascadeStyles.NONE;
@@ -205,9 +210,9 @@ private static CascadeStyle getCascadeStyle(String cascade) {
styles[i++] = CascadeStyles.getCascadeStyle( tokens.nextToken() );
}
return new CascadeStyles.MultipleCascadeStyle(styles);
- }
+ }
}
-
+
public String getCascade() {
return cascade;
}
@@ -231,7 +236,7 @@ public boolean isUpdateable() {
}
public boolean isInsertable() {
- // if the property mapping consists of all formulas,
+ // if the property mapping consists of all formulas,
// make it non-insertable
return insertable && value.hasAnyInsertableColumns();
}
@@ -318,7 +323,7 @@ public boolean isValid(Mapping mapping) throws MappingException {
public String toString() {
return getClass().getSimpleName() + '(' + name + ')';
}
-
+
public void setLazy(boolean lazy) {
this.lazy=lazy;
}
@@ -364,11 +369,11 @@ public boolean isOptimisticLocked() {
public void setOptimisticLocked(boolean optimisticLocked) {
this.optimisticLocked = optimisticLocked;
}
-
+
public boolean isOptional() {
return optional;
}
-
+
public void setOptional(boolean optional) {
this.optional = optional;
}
@@ -384,7 +389,7 @@ public void setPersistentClass(PersistentClass persistentClass) {
public boolean isSelectable() {
return selectable;
}
-
+
public void setSelectable(boolean selectable) {
this.selectable = selectable;
}
diff --git a/hibernate-core/src/main/java/org/hibernate/tuple/AbstractNonIdentifierAttribute.java b/hibernate-core/src/main/java/org/hibernate/tuple/AbstractNonIdentifierAttribute.java
index 5183f8bdb25a..088ccae7978a 100644
--- a/hibernate-core/src/main/java/org/hibernate/tuple/AbstractNonIdentifierAttribute.java
+++ b/hibernate-core/src/main/java/org/hibernate/tuple/AbstractNonIdentifierAttribute.java
@@ -7,6 +7,7 @@
package org.hibernate.tuple;
import org.hibernate.FetchMode;
+import org.hibernate.annotations.OnDeleteAction;
import org.hibernate.engine.spi.CascadeStyle;
import org.hibernate.engine.spi.SessionFactoryImplementor;
import org.hibernate.persister.walking.spi.AttributeSource;
@@ -94,6 +95,11 @@ public CascadeStyle getCascadeStyle() {
return attributeInformation.getCascadeStyle();
}
+ @Override
+ public OnDeleteAction getOnDeleteAction() {
+ return attributeInformation.getOnDeleteAction();
+ }
+
@Override
public FetchMode getFetchMode() {
return attributeInformation.getFetchMode();
diff --git a/hibernate-core/src/main/java/org/hibernate/tuple/BaselineAttributeInformation.java b/hibernate-core/src/main/java/org/hibernate/tuple/BaselineAttributeInformation.java
index fed498a92b28..b7b497c1d515 100644
--- a/hibernate-core/src/main/java/org/hibernate/tuple/BaselineAttributeInformation.java
+++ b/hibernate-core/src/main/java/org/hibernate/tuple/BaselineAttributeInformation.java
@@ -7,6 +7,7 @@
package org.hibernate.tuple;
import org.hibernate.FetchMode;
+import org.hibernate.annotations.OnDeleteAction;
import org.hibernate.engine.spi.CascadeStyle;
/**
@@ -21,6 +22,7 @@ public class BaselineAttributeInformation {
private final boolean nullable;
private final boolean dirtyCheckable;
private final boolean versionable;
+ private final OnDeleteAction onDeleteAction;
private final CascadeStyle cascadeStyle;
private final FetchMode fetchMode;
@@ -32,6 +34,7 @@ public BaselineAttributeInformation(
boolean dirtyCheckable,
boolean versionable,
CascadeStyle cascadeStyle,
+ OnDeleteAction onDeleteAction,
FetchMode fetchMode) {
this.lazy = lazy;
this.insertable = insertable;
@@ -40,6 +43,7 @@ public BaselineAttributeInformation(
this.dirtyCheckable = dirtyCheckable;
this.versionable = versionable;
this.cascadeStyle = cascadeStyle;
+ this.onDeleteAction = onDeleteAction;
this.fetchMode = fetchMode;
}
@@ -75,6 +79,10 @@ public FetchMode getFetchMode() {
return fetchMode;
}
+ public OnDeleteAction getOnDeleteAction() {
+ return onDeleteAction;
+ }
+
public static class Builder {
private boolean lazy;
private boolean insertable;
@@ -83,6 +91,7 @@ public static class Builder {
private boolean dirtyCheckable;
private boolean versionable;
private CascadeStyle cascadeStyle;
+ private OnDeleteAction onDeleteAction;
private FetchMode fetchMode;
public Builder setLazy(boolean lazy) {
@@ -120,6 +129,11 @@ public Builder setCascadeStyle(CascadeStyle cascadeStyle) {
return this;
}
+ public Builder setOnDeleteAction(OnDeleteAction onDeleteAction) {
+ this.onDeleteAction = onDeleteAction;
+ return this;
+ }
+
public Builder setFetchMode(FetchMode fetchMode) {
this.fetchMode = fetchMode;
return this;
@@ -134,6 +148,7 @@ public BaselineAttributeInformation createInformation() {
dirtyCheckable,
versionable,
cascadeStyle,
+ onDeleteAction,
fetchMode
);
}
diff --git a/hibernate-core/src/main/java/org/hibernate/tuple/NonIdentifierAttribute.java b/hibernate-core/src/main/java/org/hibernate/tuple/NonIdentifierAttribute.java
index df3f580fc750..18de4823b048 100644
--- a/hibernate-core/src/main/java/org/hibernate/tuple/NonIdentifierAttribute.java
+++ b/hibernate-core/src/main/java/org/hibernate/tuple/NonIdentifierAttribute.java
@@ -7,6 +7,7 @@
package org.hibernate.tuple;
import org.hibernate.FetchMode;
+import org.hibernate.annotations.OnDeleteAction;
import org.hibernate.engine.spi.CascadeStyle;
/**
@@ -34,5 +35,7 @@ public interface NonIdentifierAttribute extends Attribute {
CascadeStyle getCascadeStyle();
+ OnDeleteAction getOnDeleteAction();
+
FetchMode getFetchMode();
}
diff --git a/hibernate-core/src/main/java/org/hibernate/tuple/PropertyFactory.java b/hibernate-core/src/main/java/org/hibernate/tuple/PropertyFactory.java
index ecd7de0a0f68..e787729b22b8 100644
--- a/hibernate-core/src/main/java/org/hibernate/tuple/PropertyFactory.java
+++ b/hibernate-core/src/main/java/org/hibernate/tuple/PropertyFactory.java
@@ -101,6 +101,7 @@ public static VersionProperty buildVersionProperty(
.setDirtyCheckable( property.isUpdateable() && !lazy )
.setVersionable( property.isOptimisticLocked() )
.setCascadeStyle( property.getCascadeStyle() )
+ .setOnDeleteAction( property.getOnDeleteAction() )
.createInformation()
);
}
@@ -171,6 +172,7 @@ public static NonIdentifierAttribute buildEntityBasedAttribute(
.setDirtyCheckable( alwaysDirtyCheck || property.isUpdateable() )
.setVersionable( property.isOptimisticLocked() )
.setCascadeStyle( property.getCascadeStyle() )
+ .setOnDeleteAction( property.getOnDeleteAction() )
.setFetchMode( property.getValue().getFetchMode() )
.createInformation()
);
@@ -190,6 +192,7 @@ public static NonIdentifierAttribute buildEntityBasedAttribute(
.setDirtyCheckable( alwaysDirtyCheck || property.isUpdateable() )
.setVersionable( property.isOptimisticLocked() )
.setCascadeStyle( property.getCascadeStyle() )
+ .setOnDeleteAction( property.getOnDeleteAction() )
.setFetchMode( property.getValue().getFetchMode() )
.createInformation()
);
@@ -211,6 +214,7 @@ public static NonIdentifierAttribute buildEntityBasedAttribute(
.setDirtyCheckable( alwaysDirtyCheck || property.isUpdateable() )
.setVersionable( property.isOptimisticLocked() )
.setCascadeStyle( property.getCascadeStyle() )
+ .setOnDeleteAction( property.getOnDeleteAction() )
.setFetchMode( property.getValue().getFetchMode() )
.createInformation()
);
diff --git a/hibernate-core/src/main/java/org/hibernate/tuple/StandardProperty.java b/hibernate-core/src/main/java/org/hibernate/tuple/StandardProperty.java
index dd672af71e59..cdc25d1a802e 100644
--- a/hibernate-core/src/main/java/org/hibernate/tuple/StandardProperty.java
+++ b/hibernate-core/src/main/java/org/hibernate/tuple/StandardProperty.java
@@ -7,6 +7,7 @@
package org.hibernate.tuple;
import org.hibernate.FetchMode;
+import org.hibernate.annotations.OnDeleteAction;
import org.hibernate.engine.spi.CascadeStyle;
import org.hibernate.type.Type;
@@ -39,6 +40,7 @@ public StandardProperty(
boolean checkable,
boolean versionable,
CascadeStyle cascadeStyle,
+ OnDeleteAction onDeleteAction,
FetchMode fetchMode) {
super(
null,
@@ -54,6 +56,7 @@ public StandardProperty(
.setDirtyCheckable( checkable )
.setVersionable( versionable )
.setCascadeStyle( cascadeStyle )
+ .setOnDeleteAction( onDeleteAction )
.setFetchMode( fetchMode )
.createInformation()
);
diff --git a/hibernate-core/src/main/java/org/hibernate/tuple/entity/EntityMetamodel.java b/hibernate-core/src/main/java/org/hibernate/tuple/entity/EntityMetamodel.java
index 65740b2a384f..534437680543 100644
--- a/hibernate-core/src/main/java/org/hibernate/tuple/entity/EntityMetamodel.java
+++ b/hibernate-core/src/main/java/org/hibernate/tuple/entity/EntityMetamodel.java
@@ -19,6 +19,7 @@
import org.hibernate.HibernateException;
import org.hibernate.MappingException;
import org.hibernate.annotations.NotFoundAction;
+import org.hibernate.annotations.OnDeleteAction;
import org.hibernate.boot.spi.MetadataImplementor;
import org.hibernate.bytecode.enhance.spi.interceptor.EnhancementHelper;
import org.hibernate.bytecode.internal.BytecodeEnhancementMetadataNonPojoImpl;
@@ -67,6 +68,7 @@
import static org.hibernate.internal.util.collections.ArrayHelper.toIntArray;
import static org.hibernate.internal.util.collections.CollectionHelper.toSmallMap;
import static org.hibernate.internal.util.collections.CollectionHelper.toSmallSet;
+import static org.hibernate.tuple.PropertyFactory.buildIdentifierAttribute;
/**
* Centralizes metamodel information about an entity.
@@ -106,6 +108,7 @@ public class EntityMetamodel implements Serializable {
private final boolean[] propertyInsertability;
private final boolean[] propertyNullability;
private final boolean[] propertyVersionability;
+ private final OnDeleteAction[] propertyOnDeleteActions;
private final CascadeStyle[] cascadeStyles;
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -174,21 +177,19 @@ public EntityMetamodel(
subclassId = persistentClass.getSubclassId();
- identifierAttribute = PropertyFactory.buildIdentifierAttribute(
- persistentClass,
- sessionFactory.getGenerator( rootName )
- );
+ final Generator idgenerator = sessionFactory.getGenerator( rootName );
+ identifierAttribute = buildIdentifierAttribute( persistentClass, idgenerator );
versioned = persistentClass.isVersioned();
final boolean collectionsInDefaultFetchGroupEnabled =
creationContext.getSessionFactoryOptions().isCollectionsInDefaultFetchGroupEnabled();
+ final boolean supportsCascadeDelete = creationContext.getDialect().supportsCascadeDelete();
if ( persistentClass.hasPojoRepresentation() ) {
final Component identifierMapperComponent = persistentClass.getIdentifierMapper();
final CompositeType nonAggregatedCidMapper;
final Set idAttributeNames;
-
if ( identifierMapperComponent != null ) {
nonAggregatedCidMapper = (CompositeType) identifierMapperComponent.getType();
idAttributeNames = new HashSet<>( );
@@ -229,6 +230,7 @@ public EntityMetamodel(
propertyNullability = new boolean[propertySpan];
propertyVersionability = new boolean[propertySpan];
propertyLaziness = new boolean[propertySpan];
+ propertyOnDeleteActions = new OnDeleteAction[propertySpan];
cascadeStyles = new CascadeStyle[propertySpan];
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -321,7 +323,7 @@ public EntityMetamodel(
nonlazyPropertyUpdateability[i] = attribute.isUpdateable() && !lazy;
propertyCheckability[i] = propertyUpdateability[i]
|| propertyType.isAssociationType() && ( (AssociationType) propertyType ).isAlwaysDirtyChecked();
-
+ propertyOnDeleteActions[i] = supportsCascadeDelete ? attribute.getOnDeleteAction() : null;
cascadeStyles[i] = attribute.getCascadeStyle();
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -864,4 +866,8 @@ public boolean isInstrumented() {
public BytecodeEnhancementMetadata getBytecodeEnhancementMetadata() {
return bytecodeEnhancementMetadata;
}
+
+ public OnDeleteAction[] getPropertyOnDeleteActions() {
+ return propertyOnDeleteActions;
+ }
}
diff --git a/hibernate-core/src/main/java/org/hibernate/type/AnyType.java b/hibernate-core/src/main/java/org/hibernate/type/AnyType.java
index f508d441daea..64bfa3f109d2 100644
--- a/hibernate-core/src/main/java/org/hibernate/type/AnyType.java
+++ b/hibernate-core/src/main/java/org/hibernate/type/AnyType.java
@@ -21,6 +21,7 @@
import org.hibernate.MappingException;
import org.hibernate.PropertyNotFoundException;
import org.hibernate.TransientObjectException;
+import org.hibernate.annotations.OnDeleteAction;
import org.hibernate.bytecode.enhance.spi.LazyPropertyInitializer;
import org.hibernate.engine.spi.CascadeStyle;
import org.hibernate.engine.spi.CascadeStyles;
@@ -410,6 +411,11 @@ public CascadeStyle getCascadeStyle(int i) {
return CascadeStyles.NONE;
}
+ @Override
+ public OnDeleteAction getOnDeleteAction(int index) {
+ return OnDeleteAction.NO_ACTION;
+ }
+
@Override
public FetchMode getFetchMode(int i) {
return FetchMode.SELECT;
diff --git a/hibernate-core/src/main/java/org/hibernate/type/ComponentType.java b/hibernate-core/src/main/java/org/hibernate/type/ComponentType.java
index cfccfbd4767d..75f88a927324 100644
--- a/hibernate-core/src/main/java/org/hibernate/type/ComponentType.java
+++ b/hibernate-core/src/main/java/org/hibernate/type/ComponentType.java
@@ -20,6 +20,7 @@
import org.hibernate.PropertyNotFoundException;
import org.hibernate.Remove;
import org.hibernate.boot.spi.MetadataBuildingContext;
+import org.hibernate.annotations.OnDeleteAction;
import org.hibernate.bytecode.enhance.spi.LazyPropertyInitializer;
import org.hibernate.engine.spi.CascadeStyle;
import org.hibernate.engine.spi.CascadeStyles;
@@ -63,6 +64,7 @@ public class ComponentType extends AbstractType implements CompositeTypeImplemen
private final int[] originalPropertyOrder;
protected final int propertySpan;
private final CascadeStyle[] cascade;
+ private final OnDeleteAction[] onDeleteAction;
private final FetchMode[] joinedFetch;
private final int discriminatorColumnSpan;
@@ -92,11 +94,18 @@ public ComponentType(Component component, int[] originalPropertyOrder, boolean m
this.propertySpan = component.getPropertySpan();
this.originalPropertyOrder = originalPropertyOrder;
final Value discriminator = component.getDiscriminator();
- this.propertyNames = new String[propertySpan + ( component.isPolymorphic() ? 1 : 0 )];
- this.propertyTypes = new Type[propertySpan + ( component.isPolymorphic() ? 1 : 0 )];
- this.propertyNullability = new boolean[propertySpan + ( component.isPolymorphic() ? 1 : 0 )];
- this.cascade = new CascadeStyle[propertySpan + ( component.isPolymorphic() ? 1 : 0 )];
- this.joinedFetch = new FetchMode[propertySpan + ( component.isPolymorphic() ? 1 : 0 )];
+ final int length = propertySpan + (component.isPolymorphic() ? 1 : 0);
+ this.propertyNames = new String[length];
+ this.propertyTypes = new Type[length];
+ this.propertyNullability = new boolean[length];
+ this.cascade = new CascadeStyle[length];
+ this.onDeleteAction = new OnDeleteAction[length];
+ this.joinedFetch = new FetchMode[length];
+
+ final boolean supportsCascadeDelete =
+ component.getBuildingContext().getMetadataCollector()
+ .getDatabase().getDialect()
+ .supportsCascadeDelete();
int i = 0;
for ( Property property : component.getProperties() ) {
@@ -105,6 +114,7 @@ public ComponentType(Component component, int[] originalPropertyOrder, boolean m
this.propertyNullability[i] = property.isOptional();
this.cascade[i] = property.getCascadeStyle();
this.joinedFetch[i] = property.getValue().getFetchMode();
+ onDeleteAction[i] = supportsCascadeDelete ? property.getOnDeleteAction() : null;
if ( !property.isOptional() ) {
hasNotNullProperty = true;
}
@@ -594,6 +604,11 @@ public CascadeStyle getCascadeStyle(int i) {
return cascade[i];
}
+ @Override
+ public OnDeleteAction getOnDeleteAction(int i) {
+ return onDeleteAction[i];
+ }
+
@Override
public boolean isMutable() {
return mutable;
diff --git a/hibernate-core/src/main/java/org/hibernate/type/CompositeType.java b/hibernate-core/src/main/java/org/hibernate/type/CompositeType.java
index 344bad6f4253..a86fdec3c1df 100644
--- a/hibernate-core/src/main/java/org/hibernate/type/CompositeType.java
+++ b/hibernate-core/src/main/java/org/hibernate/type/CompositeType.java
@@ -10,6 +10,7 @@
import org.hibernate.FetchMode;
import org.hibernate.HibernateException;
+import org.hibernate.annotations.OnDeleteAction;
import org.hibernate.engine.spi.CascadeStyle;
import org.hibernate.engine.spi.SharedSessionContractImplementor;
@@ -124,6 +125,17 @@ default Object replacePropertyValues(Object component, Object[] values, SharedSe
*/
CascadeStyle getCascadeStyle(int index);
+ /**
+ * Retrieve the on delete action of the indicated component property.
+ *
+ * @param index The property index,
+ *
+ * @return The cascade style.
+ *
+ * @since 7.0
+ */
+ OnDeleteAction getOnDeleteAction(int index);
+
/**
* Retrieve the fetch mode of the indicated component property.
*
diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/ondeletecascade/OnDeleteCascadeRemoveTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/ondeletecascade/OnDeleteCascadeRemoveTest.java
new file mode 100644
index 000000000000..c6b26aa4806c
--- /dev/null
+++ b/hibernate-core/src/test/java/org/hibernate/orm/test/ondeletecascade/OnDeleteCascadeRemoveTest.java
@@ -0,0 +1,124 @@
+/*
+ * Hibernate, Relational Persistence for Idiomatic Java
+ *
+ * License: GNU Lesser General Public License (LGPL), version 2.1 or later.
+ * See the lgpl.txt file in the root directory or .
+ */
+package org.hibernate.orm.test.ondeletecascade;
+
+import jakarta.persistence.Cacheable;
+import jakarta.persistence.CascadeType;
+import jakarta.persistence.Entity;
+import jakarta.persistence.Id;
+import jakarta.persistence.ManyToOne;
+import jakarta.persistence.OneToMany;
+import org.hibernate.Hibernate;
+import org.hibernate.SessionFactory;
+import org.hibernate.annotations.OnDelete;
+import org.hibernate.annotations.OnDeleteAction;
+import org.hibernate.engine.spi.SessionImplementor;
+import org.hibernate.stat.EntityStatistics;
+import org.hibernate.stat.Statistics;
+import org.hibernate.testing.orm.junit.EntityManagerFactoryScope;
+import org.hibernate.testing.orm.junit.Jpa;
+import org.junit.jupiter.api.Test;
+
+import java.util.HashSet;
+import java.util.Set;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+@Jpa(annotatedClasses =
+ {OnDeleteCascadeRemoveTest.Parent.class,
+ OnDeleteCascadeRemoveTest.Child.class},
+ generateStatistics = true,
+ useCollectingStatementInspector = true)
+//@RequiresDialectFeature(feature = DialectFeatureChecks.SupportsCascadeDeleteCheck.class)
+class OnDeleteCascadeRemoveTest {
+ @Test
+ void testOnDeleteCascadeRemove1(EntityManagerFactoryScope scope) {
+ Statistics statistics =
+ scope.getEntityManagerFactory().unwrap( SessionFactory.class )
+ .getStatistics();
+ statistics.clear();
+ var inspector = scope.getCollectingStatementInspector();
+ inspector.clear();
+ scope.inTransaction( em -> {
+ Parent parent = new Parent();
+ Child child = new Child();
+ parent.children.add( child );
+ child.parent = parent;
+ em.persist( parent );
+ } );
+ scope.inTransaction( em -> {
+ Parent parent = em.find( Parent.class, 0L );
+ assertFalse( Hibernate.isInitialized( parent.children ) );
+ em.remove( parent );
+ // note: ideally we would skip the initialization here
+ assertTrue( Hibernate.isInitialized( parent.children ) );
+ });
+ EntityStatistics entityStatistics = statistics.getEntityStatistics( Child.class.getName() );
+ assertEquals( 1L, entityStatistics.getDeleteCount() );
+ inspector.assertExecutedCount( scope.getDialect().supportsCascadeDelete() ? 5 : 6 );
+ long children =
+ scope.fromTransaction( em -> em.createQuery( "select count(*) from CascadeChild", Long.class )
+ .getSingleResult() );
+ assertEquals( 0L, children );
+ }
+
+ @Test
+ void testOnDeleteCascadeRemove2(EntityManagerFactoryScope scope) {
+ Statistics statistics =
+ scope.getEntityManagerFactory().unwrap( SessionFactory.class )
+ .getStatistics();
+ statistics.clear();
+ var inspector = scope.getCollectingStatementInspector();
+ inspector.clear();
+ scope.inTransaction( em -> {
+ Parent parent = new Parent();
+ Child child = new Child();
+ parent.children.add( child );
+ child.parent = parent;
+ em.persist( parent );
+ } );
+ scope.inTransaction( em -> {
+ Parent parent = em.find( Parent.class, 0L );
+ assertEquals(1, parent.children.size());
+ assertTrue( Hibernate.isInitialized( parent.children ) );
+ em.remove( parent );
+ assertTrue( em.unwrap( SessionImplementor.class )
+ .getPersistenceContext()
+ .getEntry( parent.children.iterator().next() )
+ .getStatus().isDeletedOrGone() );
+ });
+ EntityStatistics entityStatistics = statistics.getEntityStatistics( Child.class.getName() );
+ assertEquals( 1L, entityStatistics.getDeleteCount() );
+ inspector.assertExecutedCount( scope.getDialect().supportsCascadeDelete() ? 5 : 6 );
+ long children =
+ scope.fromTransaction( em -> em.createQuery( "select count(c.id) from CascadeChild c", Long.class )
+ .getSingleResult() );
+ assertEquals( 0L, children );
+ }
+
+ @Entity(name="CascadeParent")
+ static class Parent {
+ @Id
+ long id;
+ @OneToMany(mappedBy = "parent",
+ cascade = {CascadeType.PERSIST, CascadeType.REMOVE})
+ @OnDelete(action = OnDeleteAction.CASCADE)
+ Set children = new HashSet<>();
+ }
+ @Entity(name="CascadeChild")
+ @Cacheable
+// @SQLDelete( sql = "should never happen" )
+ static class Child {
+ @Id
+ long id;
+ @OnDelete(action = OnDeleteAction.CASCADE)
+ @ManyToOne
+ Parent parent;
+ }
+}
diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/ondeletecascade/OnDeleteCollectionTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/ondeletecascade/OnDeleteCollectionTest.java
new file mode 100644
index 000000000000..9975c135dbde
--- /dev/null
+++ b/hibernate-core/src/test/java/org/hibernate/orm/test/ondeletecascade/OnDeleteCollectionTest.java
@@ -0,0 +1,82 @@
+/*
+ * Hibernate, Relational Persistence for Idiomatic Java
+ *
+ * License: GNU Lesser General Public License (LGPL), version 2.1 or later.
+ * See the lgpl.txt file in the root directory or .
+ */
+package org.hibernate.orm.test.ondeletecascade;
+
+import org.hibernate.Hibernate;
+import org.hibernate.annotations.OnDelete;
+import org.hibernate.annotations.OnDeleteAction;
+
+import org.hibernate.testing.orm.junit.EntityManagerFactoryScope;
+import org.hibernate.testing.orm.junit.Jira;
+import org.hibernate.testing.orm.junit.Jpa;
+import org.junit.jupiter.api.Test;
+
+import jakarta.persistence.ElementCollection;
+import jakarta.persistence.Entity;
+import jakarta.persistence.Id;
+import java.util.HashSet;
+import java.util.Set;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+@Jpa(annotatedClasses =
+ {OnDeleteCollectionTest.A.class},
+ useCollectingStatementInspector = true)
+class OnDeleteCollectionTest {
+ @Test
+ void test(EntityManagerFactoryScope scope) {
+ var inspector = scope.getCollectingStatementInspector();
+ inspector.clear();
+
+ scope.inTransaction( em -> {
+ A a = new A();
+ a.id = 2;
+ a.bs.add( "b" );
+ em.persist( a );
+ } );
+ inspector.assertExecutedCount( 2 );
+ inspector.clear();
+
+ scope.inTransaction( em -> {
+ A a = em.find( A.class, 2L );
+ inspector.assertExecutedCount( 1 );
+ assertEquals( 1, a.bs.size() );
+ inspector.assertExecutedCount( 2 );
+ assertTrue( Hibernate.isInitialized( a.bs ) );
+ } );
+ inspector.clear();
+
+ scope.inTransaction( em -> {
+ A a = em.find( A.class, 2L );
+ inspector.assertExecutedCount( 1 );
+ em.remove( a );
+ assertFalse( Hibernate.isInitialized( a.bs ) );
+ } );
+ inspector.assertExecutedCount( scope.getDialect().supportsCascadeDelete() ? 2 : 3 );
+
+ scope.inTransaction( em -> {
+ assertEquals( 0,
+ em.createNativeQuery( "select count(*) from A_bs", Integer.class )
+ .getSingleResult() );
+ assertEquals( 0,
+ em.createNativeQuery( "select count(*) from A", Integer.class )
+ .getSingleResult() );
+ });
+ }
+
+ @Entity(name = "A")
+ static class A {
+ @Id
+ long id;
+ boolean a;
+ @ElementCollection
+ @OnDelete(action = OnDeleteAction.CASCADE)
+ Set bs = new HashSet<>();
+ }
+}
diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/ondeletecascade/OnDeleteJoinedInheritanceTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/ondeletecascade/OnDeleteJoinedInheritanceTest.java
new file mode 100644
index 000000000000..68e76bf55ebd
--- /dev/null
+++ b/hibernate-core/src/test/java/org/hibernate/orm/test/ondeletecascade/OnDeleteJoinedInheritanceTest.java
@@ -0,0 +1,84 @@
+/*
+ * Hibernate, Relational Persistence for Idiomatic Java
+ *
+ * License: GNU Lesser General Public License (LGPL), version 2.1 or later.
+ * See the lgpl.txt file in the root directory or .
+ */
+package org.hibernate.orm.test.ondeletecascade;
+
+import org.hibernate.annotations.OnDelete;
+import org.hibernate.annotations.OnDeleteAction;
+
+import org.hibernate.testing.orm.junit.EntityManagerFactoryScope;
+import org.hibernate.testing.orm.junit.Jpa;
+import org.junit.jupiter.api.Test;
+
+import jakarta.persistence.Entity;
+import jakarta.persistence.Id;
+import jakarta.persistence.Inheritance;
+import jakarta.persistence.InheritanceType;
+
+import static org.junit.Assert.assertEquals;
+
+@Jpa(annotatedClasses =
+ {OnDeleteJoinedInheritanceTest.A.class,
+ OnDeleteJoinedInheritanceTest.B.class,
+ OnDeleteJoinedInheritanceTest.C.class},
+ useCollectingStatementInspector = true)
+//@RequiresDialectFeature(feature = DialectFeatureChecks.SupportsCascadeDeleteCheck.class)
+class OnDeleteJoinedInheritanceTest {
+ @Test void test(EntityManagerFactoryScope scope) {
+ var inspector = scope.getCollectingStatementInspector();
+ scope.inTransaction( em -> {
+ B b = new B();
+ b.id = 1;
+ em.persist( b );
+ C c = new C();
+ c.id = 2;
+ em.persist( c );
+ } );
+ inspector.assertExecutedCount( 4 );
+ inspector.clear();
+
+ scope.inTransaction( em -> {
+ A b = em.find( A.class, 1L );
+ A c = em.getReference( A.class, 2L );
+ inspector.assertExecutedCount( 1 );
+ em.remove( b );
+ em.remove( c );
+ } );
+ inspector.assertExecutedCount( scope.getDialect().supportsCascadeDelete() ? 4 : 6 );
+
+ scope.inTransaction( em -> {
+ assertEquals( 0,
+ em.createNativeQuery( "select count(*) from B", Integer.class )
+ .getSingleResult() );
+ assertEquals( 0,
+ em.createNativeQuery( "select count(*) from C", Integer.class )
+ .getSingleResult() );
+ assertEquals( 0,
+ em.createNativeQuery( "select count(*) from A", Integer.class )
+ .getSingleResult() );
+ });
+ }
+
+ @Entity(name = "A")
+ @Inheritance(strategy = InheritanceType.JOINED)
+ static class A {
+ @Id
+ long id;
+ boolean a;
+ }
+
+ @Entity(name = "B")
+ @OnDelete(action = OnDeleteAction.CASCADE)
+ static class B extends A {
+ long b;
+ }
+
+ @Entity(name = "C")
+ @OnDelete(action = OnDeleteAction.CASCADE)
+ static class C extends A {
+ int c;
+ }
+}
diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/ondeletecascade/OnDeleteManyToOneTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/ondeletecascade/OnDeleteManyToOneTest.java
new file mode 100644
index 000000000000..e3bec0798c70
--- /dev/null
+++ b/hibernate-core/src/test/java/org/hibernate/orm/test/ondeletecascade/OnDeleteManyToOneTest.java
@@ -0,0 +1,106 @@
+/*
+ * Hibernate, Relational Persistence for Idiomatic Java
+ *
+ * License: GNU Lesser General Public License (LGPL), version 2.1 or later.
+ * See the lgpl.txt file in the root directory or .
+ */
+package org.hibernate.orm.test.ondeletecascade;
+
+import org.hibernate.annotations.OnDelete;
+import org.hibernate.internal.SessionFactoryImpl;
+
+import org.hibernate.testing.orm.junit.DialectFeatureChecks;
+import org.hibernate.testing.orm.junit.EntityManagerFactoryScope;
+import org.hibernate.testing.orm.junit.Jpa;
+import org.hibernate.testing.orm.junit.RequiresDialectFeature;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+
+import jakarta.persistence.Entity;
+import jakarta.persistence.Id;
+import jakarta.persistence.ManyToOne;
+import jakarta.persistence.OneToMany;
+import java.util.HashSet;
+import java.util.Set;
+
+import static jakarta.persistence.FetchType.EAGER;
+import static org.hibernate.annotations.OnDeleteAction.CASCADE;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+@Jpa(annotatedClasses = {OnDeleteManyToOneTest.Parent.class, OnDeleteManyToOneTest.Child.class})
+@RequiresDialectFeature(feature = DialectFeatureChecks.SupportsCascadeDeleteCheck.class)
+public class OnDeleteManyToOneTest {
+ @Test
+ public void testOnDelete(EntityManagerFactoryScope scope) {
+ Parent parent = new Parent();
+ Child child = new Child();
+ child.parent = parent;
+ parent.children.add( child );
+ scope.inTransaction( em -> {
+ em.persist( parent );
+ em.persist( child );
+ } );
+ scope.inTransaction( em -> {
+ Parent p = em.find( Parent.class, parent.id );
+ em.remove( p );
+ } );
+ scope.inTransaction( em -> {
+ assertNull( em.find( Child.class, child.id ) );
+ } );
+ }
+
+ @Test
+ public void testOnDeleteReference(EntityManagerFactoryScope scope) {
+ Parent parent = new Parent();
+ Child child = new Child();
+ child.parent = parent;
+ parent.children.add( child );
+ scope.inTransaction( em -> {
+ em.persist( parent );
+ em.persist( child );
+ } );
+ scope.inTransaction( em -> em.remove( em.getReference( Parent.class, parent.id ) ) );
+ scope.inTransaction( em -> assertNull( em.find( Child.class, child.id ) ) );
+ }
+
+ @Test
+ public void testOnDeleteInReverse(EntityManagerFactoryScope scope) {
+ Parent parent = new Parent();
+ Child child = new Child();
+ child.parent = parent;
+ parent.children.add( child );
+ scope.inTransaction( em -> {
+ em.persist( parent );
+ em.persist( child );
+ } );
+ scope.inTransaction( em -> {
+ Child c = em.find( Child.class, child.id );
+ em.remove( c );
+ } );
+ scope.inTransaction( em -> {
+ assertNull( em.find( Child.class, child.id ) );
+ } );
+ }
+
+ @AfterEach
+ public void tearDown(EntityManagerFactoryScope scope) {
+ scope.getEntityManagerFactory().unwrap( SessionFactoryImpl.class ).getSchemaManager().truncateMappedObjects();
+ }
+
+ @Entity
+ static class Parent {
+ @Id
+ long id;
+ @OneToMany(mappedBy = "parent", fetch = EAGER)
+ Set children = new HashSet<>();
+ }
+
+ @Entity
+ static class Child {
+ @Id
+ long id;
+ @ManyToOne
+ @OnDelete(action = CASCADE)
+ Parent parent;
+ }
+}
diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/ondeletecascade/OnDeleteOneToManyTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/ondeletecascade/OnDeleteOneToManyTest.java
new file mode 100644
index 000000000000..fa75150ca88f
--- /dev/null
+++ b/hibernate-core/src/test/java/org/hibernate/orm/test/ondeletecascade/OnDeleteOneToManyTest.java
@@ -0,0 +1,107 @@
+/*
+ * Hibernate, Relational Persistence for Idiomatic Java
+ *
+ * License: GNU Lesser General Public License (LGPL), version 2.1 or later.
+ * See the lgpl.txt file in the root directory or .
+ */
+package org.hibernate.orm.test.ondeletecascade;
+
+import org.hibernate.Hibernate;
+import org.hibernate.TransientObjectException;
+import org.hibernate.annotations.OnDelete;
+import org.hibernate.annotations.OnDeleteAction;
+import org.hibernate.internal.SessionFactoryImpl;
+
+import org.hibernate.testing.orm.junit.EntityManagerFactoryScope;
+import org.hibernate.testing.orm.junit.Jpa;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+
+import jakarta.persistence.Entity;
+import jakarta.persistence.Id;
+import jakarta.persistence.JoinColumn;
+import jakarta.persistence.OneToMany;
+import jakarta.persistence.RollbackException;
+import java.util.HashSet;
+import java.util.Set;
+
+import static jakarta.persistence.FetchType.EAGER;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+@Jpa(annotatedClasses =
+ {OnDeleteOneToManyTest.Parent.class, OnDeleteOneToManyTest.Child.class},
+ useCollectingStatementInspector = true)
+//@RequiresDialectFeature(feature = DialectFeatureChecks.SupportsCascadeDeleteCheck.class)
+public class OnDeleteOneToManyTest {
+ @Test
+ public void testOnDeleteParent(EntityManagerFactoryScope scope) {
+ var inspector = scope.getCollectingStatementInspector();
+ inspector.clear();
+ Parent parent = new Parent();
+ Child child = new Child();
+ parent.children.add( child );
+ scope.inTransaction( em -> {
+ em.persist( parent );
+ em.persist( child );
+ } );
+ inspector.assertExecutedCount( 3 );
+ inspector.clear();
+ scope.inTransaction( em -> {
+ Parent p = em.find( Parent.class, parent.id );
+ inspector.assertExecutedCount( 1 );
+ assertTrue( Hibernate.isInitialized( p.children ) );
+ em.remove( p );
+ } );
+ inspector.assertExecutedCount( 3 );
+ scope.inTransaction( em -> {
+ // since it's an owned collection, the FK gets set to null
+ assertNotNull( em.find( Child.class, child.id ) );
+ } );
+ }
+
+ @Test
+ public void testOnDeleteChildrenFails(EntityManagerFactoryScope scope) {
+ Parent parent = new Parent();
+ Child child = new Child();
+ parent.children.add( child );
+ scope.inTransaction( em -> {
+ em.persist( parent );
+ em.persist( child );
+ } );
+ try {
+ scope.inTransaction( em -> {
+ Parent p = em.find( Parent.class, parent.id );
+ for ( Child c : p.children ) {
+ em.remove( c );
+ }
+ } );
+ fail();
+ }
+ catch (RollbackException re) {
+ assertTrue(re.getCause().getCause() instanceof TransientObjectException);
+ }
+ }
+
+ @AfterEach
+ public void tearDown(EntityManagerFactoryScope scope) {
+ scope.getEntityManagerFactory().unwrap( SessionFactoryImpl.class ).getSchemaManager().truncateMappedObjects();
+ }
+
+ @Entity
+ static class Parent {
+ @Id
+ long id;
+ @OneToMany(fetch = EAGER)
+ @JoinColumn(name = "parent_id")
+ @OnDelete(action = OnDeleteAction.CASCADE)
+ Set children = new HashSet<>();
+ }
+
+ @Entity
+ static class Child {
+ @Id
+ long id;
+ }
+}