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;
+ * 
+ * + *

* 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; + } +}