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 e061e4aa877e..a317f8263972 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.bytecode.enhance.spi.interceptor.LazyAttributeLoadingInterceptor; import org.hibernate.collection.spi.PersistentCollection; import org.hibernate.engine.spi.CascadeStyle; @@ -117,6 +118,8 @@ public static void cascade( hasUninitializedLazyProperties && !persister.getBytecodeEnhancementMetadata() .isAttributeLoaded( parent, propertyName ); + final boolean isCascadeDeleteEnabled = + persister.getEntityMetamodel().getPropertyOnDeleteActions()[i] == OnDeleteAction.CASCADE; if ( style.doCascade( action ) ) { final Object child; @@ -178,7 +181,7 @@ else if ( action.performOnLazyProperty() && type instanceof EntityType ) { style, propertyName, anything, - false + isCascadeDeleteEnabled ); } else { @@ -193,7 +196,7 @@ else if ( action.performOnLazyProperty() && type instanceof EntityType ) { type, style, propertyName, - false + isCascadeDeleteEnabled ); } } 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 57371f90a6c3..c9a5c76ed824 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 @@ -340,7 +340,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 @@ -387,7 +387,7 @@ public String toString() { } }; - 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() @@ -402,7 +402,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 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 5af4284ab0a3..a55d6ee3a785 100644 --- a/hibernate-core/src/main/java/org/hibernate/mapping/Property.java +++ b/hibernate-core/src/main/java/org/hibernate/mapping/Property.java @@ -12,6 +12,7 @@ import org.hibernate.HibernateException; import org.hibernate.Internal; import org.hibernate.MappingException; +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; @@ -134,6 +135,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 070d2fa8c020..5c2e85502368 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 2eb55f65d61a..819a98719cf0 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 58b5f134b0e1..70f8135263f5 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 cded42a70c28..c76743bb85f1 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 57a314ae5121..0bea43a1b75e 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 744f3417fa98..e040dbde3d93 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; // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -162,7 +165,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(); @@ -174,18 +177,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<>( ); @@ -226,6 +229,7 @@ public EntityMetamodel( propertyNullability = new boolean[propertySpan]; propertyVersionability = new boolean[propertySpan]; propertyLaziness = new boolean[propertySpan]; + propertyOnDeleteActions = new OnDeleteAction[propertySpan]; cascadeStyles = new CascadeStyle[propertySpan]; // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -318,7 +322,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(); // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -887,4 +891,8 @@ public boolean isInstrumented() { public BytecodeEnhancementMetadata getBytecodeEnhancementMetadata() { return bytecodeEnhancementMetadata; } + + public OnDeleteAction[] getPropertyOnDeleteActions() { + return propertyOnDeleteActions; + } } 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..77c8974073c0 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/OnDeleteTest.java @@ -0,0 +1,99 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * 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..bf61d6c9c349 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/OnDeleteTest2.java @@ -0,0 +1,92 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * 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; + } +}