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 bca7f71770db..a06647b2ba1f 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 @@ -10,6 +10,7 @@ import java.util.List; import org.hibernate.HibernateException; +import org.hibernate.annotations.OnDeleteAction; import org.hibernate.collection.spi.PersistentCollection; import org.hibernate.engine.spi.CascadeStyle; import org.hibernate.engine.spi.CascadingAction; @@ -111,6 +112,7 @@ public static void cascade( final boolean isUninitializedProperty = hasUninitializedLazyProperties && !bytecodeEnhancement.isAttributeLoaded( parent, propertyName ); + final boolean isCascadeDeleteEnabled = cascadeDeleteEnabled( action, persister, i ); if ( action.appliesTo( type, style ) ) { final Object child; @@ -170,7 +172,7 @@ else if ( action.performOnLazyProperty() && type instanceof EntityType ) { style, propertyName, anything, - false + isCascadeDeleteEnabled ); } else if ( action.deleteOrphans() @@ -186,7 +188,7 @@ && isLogicalOneToOne( type ) ) { type, style, propertyName, - false + isCascadeDeleteEnabled ); } } @@ -421,7 +423,7 @@ private static void cascadeComponent( componentPropertyStyle, subPropertyName, anything, - false + cascadeDeleteEnabled( action, componentType, i ) ); } } @@ -508,7 +510,7 @@ private static void cascadeCollection( style, elemType, anything, - persister.isCascadeDeleteEnabled() + cascadeDeleteEnabled( action, persister ) ); } } @@ -607,7 +609,8 @@ private static void cascadeCollectionElements( final PersistentCollection persistentCollection = child instanceof PersistentCollection collection ? collection - : eventSource.getPersistenceContext().getCollectionHolder( child ); + : eventSource.getPersistenceContextInternal() + .getCollectionHolder( child ); final boolean deleteOrphans = style.hasOrphanDelete() && action.deleteOrphans() @@ -654,4 +657,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 fbb901c8ddd9..a833ba815f9f 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 @@ -8,16 +8,20 @@ import java.util.List; import org.checkerframework.checker.nullness.qual.Nullable; +import org.hibernate.Incubating; import org.hibernate.engine.internal.CascadePoint; import org.hibernate.event.spi.EventSource; import org.hibernate.persister.entity.EntityPersister; import org.hibernate.type.AssociationType; 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 */ @@ -32,9 +36,9 @@ public interface CascadingAction { * @param parentEntityName The name of the parent entity * @param propertyName The name of the attribute of the parent entity being cascaded * @param attributePath The full path of the attribute of the parent entity being cascaded - * @param anything Anything ;) Typically some form of cascade-local cache - * which is specific to each {@link 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, @@ -111,4 +115,17 @@ boolean cascadeNow( CascadePoint cascadePoint, AssociationType associationType, SessionFactoryImplementor factory); + + /** + * 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,String,String,List,Object,boolean)} + * for multiple purposes. + * + * @since 7 + */ + @Incubating @Nullable + ForeignKeyDirection directionAffectedByCascadeDelete(); } 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 d41923b4ff99..f92b35186de1 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 @@ -4,6 +4,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; @@ -21,6 +22,7 @@ import org.hibernate.persister.entity.EntityPersister; import org.hibernate.type.AssociationType; import org.hibernate.type.CollectionType; +import org.hibernate.type.ForeignKeyDirection; import org.hibernate.type.ManyToOneType; import org.hibernate.type.OneToOneType; import org.hibernate.type.Type; @@ -89,6 +91,11 @@ public boolean anythingToCascade(EntityPersister persister) { return persister.hasCascadeDelete(); } + @Override + public ForeignKeyDirection directionAffectedByCascadeDelete() { + return ForeignKeyDirection.FROM_PARENT; + } + @Override public String toString() { return "ACTION_DELETE"; @@ -387,7 +394,7 @@ public void cascade( Void nothing, boolean isCascadeDeleteEnabled) throws HibernateException { - if ( child != null && isChildTransient( session, child, childEntityName ) ) { + if ( child != null && isChildTransient( session, child, childEntityName, isCascadeDeleteEnabled ) ) { throw new TransientPropertyValueException( "Persistent instance of '" + parentEntityName + "' references an unsaved transient instance of '" + childEntityName @@ -481,13 +488,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() @@ -502,7 +514,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 @@ -580,6 +596,11 @@ public boolean cascadeNow( SessionFactoryImplementor factory) { return associationType.getForeignKeyDirection().cascadeNow( cascadePoint ); } + + @Override @Nullable + public ForeignKeyDirection directionAffectedByCascadeDelete() { + return null; + } } /** diff --git a/hibernate-core/src/main/java/org/hibernate/mapping/Property.java b/hibernate-core/src/main/java/org/hibernate/mapping/Property.java index 7b5406f41d99..91fdcc69f1a2 100644 --- a/hibernate-core/src/main/java/org/hibernate/mapping/Property.java +++ b/hibernate-core/src/main/java/org/hibernate/mapping/Property.java @@ -14,6 +14,7 @@ import org.hibernate.Internal; import org.hibernate.MappingException; import org.hibernate.annotations.CascadeType; +import org.hibernate.annotations.OnDeleteAction; import org.hibernate.boot.model.relational.Database; import org.hibernate.boot.model.relational.SqlStringGenerationContext; import org.hibernate.bytecode.enhance.spi.interceptor.EnhancementHelper; @@ -139,6 +140,10 @@ public void resetOptional(boolean optional) { } } + public OnDeleteAction getOnDeleteAction() { + return value instanceof ToOne toOne ? toOne.getOnDeleteAction() : null; + } + public CascadeStyle getCascadeStyle() throws MappingException { final Type type = value.getType(); if ( type instanceof AnyType ) { 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 fdfcaffe3bc9..520e5befc1f3 100644 --- a/hibernate-core/src/main/java/org/hibernate/tuple/AbstractNonIdentifierAttribute.java +++ b/hibernate-core/src/main/java/org/hibernate/tuple/AbstractNonIdentifierAttribute.java @@ -5,6 +5,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; @@ -92,6 +93,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 2fd57c3dbf16..bfabdc3d924b 100644 --- a/hibernate-core/src/main/java/org/hibernate/tuple/BaselineAttributeInformation.java +++ b/hibernate-core/src/main/java/org/hibernate/tuple/BaselineAttributeInformation.java @@ -5,6 +5,7 @@ package org.hibernate.tuple; import org.hibernate.FetchMode; +import org.hibernate.annotations.OnDeleteAction; import org.hibernate.engine.spi.CascadeStyle; /** @@ -19,6 +20,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; @@ -30,6 +32,7 @@ public BaselineAttributeInformation( boolean dirtyCheckable, boolean versionable, CascadeStyle cascadeStyle, + OnDeleteAction onDeleteAction, FetchMode fetchMode) { this.lazy = lazy; this.insertable = insertable; @@ -38,6 +41,7 @@ public BaselineAttributeInformation( this.dirtyCheckable = dirtyCheckable; this.versionable = versionable; this.cascadeStyle = cascadeStyle; + this.onDeleteAction = onDeleteAction; this.fetchMode = fetchMode; } @@ -73,6 +77,10 @@ public FetchMode getFetchMode() { return fetchMode; } + public OnDeleteAction getOnDeleteAction() { + return onDeleteAction; + } + public static class Builder { private boolean lazy; private boolean insertable; @@ -81,6 +89,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) { @@ -118,6 +127,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; @@ -132,6 +146,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 832bcc3d953d..dca7c3ea45ac 100644 --- a/hibernate-core/src/main/java/org/hibernate/tuple/NonIdentifierAttribute.java +++ b/hibernate-core/src/main/java/org/hibernate/tuple/NonIdentifierAttribute.java @@ -5,6 +5,7 @@ package org.hibernate.tuple; import org.hibernate.FetchMode; +import org.hibernate.annotations.OnDeleteAction; import org.hibernate.engine.spi.CascadeStyle; /** @@ -32,5 +33,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 f8dd796bc2c9..ea51dbea8405 100644 --- a/hibernate-core/src/main/java/org/hibernate/tuple/PropertyFactory.java +++ b/hibernate-core/src/main/java/org/hibernate/tuple/PropertyFactory.java @@ -99,6 +99,7 @@ public static VersionProperty buildVersionProperty( .setDirtyCheckable( property.isUpdateable() && !lazy ) .setVersionable( property.isOptimisticLocked() ) .setCascadeStyle( property.getCascadeStyle() ) + .setOnDeleteAction( property.getOnDeleteAction() ) .createInformation() ); } @@ -169,6 +170,7 @@ public static NonIdentifierAttribute buildEntityBasedAttribute( .setDirtyCheckable( alwaysDirtyCheck || property.isUpdateable() ) .setVersionable( property.isOptimisticLocked() ) .setCascadeStyle( property.getCascadeStyle() ) + .setOnDeleteAction( property.getOnDeleteAction() ) .setFetchMode( property.getValue().getFetchMode() ) .createInformation() ); @@ -188,6 +190,7 @@ public static NonIdentifierAttribute buildEntityBasedAttribute( .setDirtyCheckable( alwaysDirtyCheck || property.isUpdateable() ) .setVersionable( property.isOptimisticLocked() ) .setCascadeStyle( property.getCascadeStyle() ) + .setOnDeleteAction( property.getOnDeleteAction() ) .setFetchMode( property.getValue().getFetchMode() ) .createInformation() ); @@ -209,6 +212,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 f6a5e6eb569a..c01431444357 100644 --- a/hibernate-core/src/main/java/org/hibernate/tuple/StandardProperty.java +++ b/hibernate-core/src/main/java/org/hibernate/tuple/StandardProperty.java @@ -5,6 +5,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; @@ -37,6 +38,7 @@ public StandardProperty( boolean checkable, boolean versionable, CascadeStyle cascadeStyle, + OnDeleteAction onDeleteAction, FetchMode fetchMode) { super( null, @@ -52,6 +54,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 301902ccdfc1..9cb4b41d9712 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 @@ -18,6 +18,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; @@ -63,6 +64,7 @@ import static org.hibernate.internal.util.ReflectHelper.isFinalClass; import static org.hibernate.internal.util.collections.ArrayHelper.toIntArray; import static org.hibernate.internal.util.collections.CollectionHelper.toSmallSet; +import static org.hibernate.tuple.PropertyFactory.buildIdentifierAttribute; /** * Centralizes metamodel information about an entity. @@ -102,6 +104,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; // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -164,7 +167,7 @@ public EntityMetamodel( EntityPersister persister, RuntimeModelCreationContext creationContext, Function generatorSupplier) { - this.sessionFactory = creationContext.getSessionFactory(); + sessionFactory = creationContext.getSessionFactory(); // Improves performance of EntityKey#equals by avoiding content check in String#equals name = persistentClass.getEntityName().intern(); @@ -176,18 +179,18 @@ public EntityMetamodel( subclassId = persistentClass.getSubclassId(); final Generator idgenerator = generatorSupplier.apply( rootName ); - identifierAttribute = PropertyFactory.buildIdentifierAttribute( persistentClass, idgenerator ); + 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 = identifierMapperComponent.getType(); idAttributeNames = new HashSet<>( ); @@ -228,6 +231,7 @@ public EntityMetamodel( propertyNullability = new boolean[propertySpan]; propertyVersionability = new boolean[propertySpan]; propertyLaziness = new boolean[propertySpan]; + propertyOnDeleteActions = new OnDeleteAction[propertySpan]; cascadeStyles = new CascadeStyle[propertySpan]; // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -322,7 +326,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(); // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -925,4 +929,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 5aa9eb4e4ea1..aadea86f0b5c 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/AnyType.java +++ b/hibernate-core/src/main/java/org/hibernate/type/AnyType.java @@ -11,6 +11,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; @@ -429,6 +430,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 62c6a4c33acf..da26730cabe8 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/ComponentType.java +++ b/hibernate-core/src/main/java/org/hibernate/type/ComponentType.java @@ -16,6 +16,7 @@ import org.hibernate.HibernateException; import org.hibernate.MappingException; import org.hibernate.PropertyNotFoundException; +import org.hibernate.annotations.OnDeleteAction; import org.hibernate.bytecode.enhance.spi.LazyPropertyInitializer; import org.hibernate.engine.spi.CascadeStyle; import org.hibernate.engine.spi.CascadeStyles; @@ -60,6 +61,7 @@ public class ComponentType extends AbstractType private final int[] originalPropertyOrder; protected final int propertySpan; private final CascadeStyle[] cascade; + private final OnDeleteAction[] onDeleteAction; private final FetchMode[] joinedFetch; private final int discriminatorColumnSpan; @@ -84,11 +86,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() ) { @@ -97,6 +106,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; } @@ -583,6 +593,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 3c3f71b2a662..2a4540418b4f 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/CompositeType.java +++ b/hibernate-core/src/main/java/org/hibernate/type/CompositeType.java @@ -8,6 +8,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; @@ -122,6 +123,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/OnDeleteTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/OnDeleteTest.java new file mode 100644 index 000000000000..9234974a66e2 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/OnDeleteTest.java @@ -0,0 +1,99 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; +import org.hibernate.annotations.OnDelete; +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 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 = {OnDeleteTest.Parent.class, OnDeleteTest.Child.class}) +public class OnDeleteTest { + @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 ) ) ); + 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().getSchemaManager().truncate(); + } + + @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/OnDeleteTest2.java b/hibernate-core/src/test/java/org/hibernate/orm/test/OnDeleteTest2.java new file mode 100644 index 000000000000..9d5cc3aeb3aa --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/OnDeleteTest2.java @@ -0,0 +1,92 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.OneToMany; +import jakarta.persistence.RollbackException; +import org.hibernate.TransientObjectException; +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.AfterEach; +import org.junit.jupiter.api.Test; + +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 = {OnDeleteTest2.Parent.class, OnDeleteTest2.Child.class}) +public class OnDeleteTest2 { + @Test + public void testOnDeleteParent(EntityManagerFactoryScope scope) { + Parent parent = new Parent(); + Child child = new Child(); + 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 -> { + // 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().getSchemaManager().truncate(); + } + + @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; + } +}