HHH-4301 eliminate unnecessary DELETE for collections with @OnDelete(CASCADE)
This change works for @ElementCollection and @ManyToMany In future we could do a similar thing for @OneToMany and SET_NULL Signed-off-by: Gavin King <gavin@hibernate.org>
This commit is contained in:
parent
ad8fe58cf1
commit
1b67ebee60
|
@ -48,7 +48,8 @@ public final class QueuedOperationCollectionAction extends CollectionAction {
|
||||||
|
|
||||||
// TODO: It would be nice if this could be done safely by CollectionPersister#processQueuedOps;
|
// TODO: It would be nice if this could be done safely by CollectionPersister#processQueuedOps;
|
||||||
// Can't change the SPI to do this though.
|
// Can't change the SPI to do this though.
|
||||||
((AbstractPersistentCollection<?>) getCollection() ).clearOperationQueue();
|
AbstractPersistentCollection<?> collection = (AbstractPersistentCollection<?>) getCollection();
|
||||||
|
collection.clearOperationQueue();
|
||||||
|
|
||||||
// The other CollectionAction types call CollectionEntry#afterAction, which
|
// The other CollectionAction types call CollectionEntry#afterAction, which
|
||||||
// clears the dirty flag. We don't want to call CollectionEntry#afterAction unless
|
// clears the dirty flag. We don't want to call CollectionEntry#afterAction unless
|
||||||
|
|
|
@ -65,7 +65,6 @@ import org.hibernate.annotations.OptimisticLock;
|
||||||
import org.hibernate.annotations.Parameter;
|
import org.hibernate.annotations.Parameter;
|
||||||
import org.hibernate.annotations.Persister;
|
import org.hibernate.annotations.Persister;
|
||||||
import org.hibernate.annotations.QueryCacheLayout;
|
import org.hibernate.annotations.QueryCacheLayout;
|
||||||
import org.hibernate.annotations.ResultCheckStyle;
|
|
||||||
import org.hibernate.annotations.SQLDelete;
|
import org.hibernate.annotations.SQLDelete;
|
||||||
import org.hibernate.annotations.SQLDeleteAll;
|
import org.hibernate.annotations.SQLDeleteAll;
|
||||||
import org.hibernate.annotations.SQLInsert;
|
import org.hibernate.annotations.SQLInsert;
|
||||||
|
@ -97,7 +96,6 @@ import org.hibernate.engine.spi.FilterDefinition;
|
||||||
import org.hibernate.internal.CoreMessageLogger;
|
import org.hibernate.internal.CoreMessageLogger;
|
||||||
import org.hibernate.internal.util.collections.CollectionHelper;
|
import org.hibernate.internal.util.collections.CollectionHelper;
|
||||||
import org.hibernate.jdbc.Expectation;
|
import org.hibernate.jdbc.Expectation;
|
||||||
import org.hibernate.jdbc.Expectations;
|
|
||||||
import org.hibernate.mapping.Any;
|
import org.hibernate.mapping.Any;
|
||||||
import org.hibernate.mapping.Backref;
|
import org.hibernate.mapping.Backref;
|
||||||
import org.hibernate.mapping.CheckConstraint;
|
import org.hibernate.mapping.CheckConstraint;
|
||||||
|
@ -520,7 +518,6 @@ public abstract class CollectionBinder {
|
||||||
final Class<?> targetElement = elementCollectionAnn.targetClass();
|
final Class<?> targetElement = elementCollectionAnn.targetClass();
|
||||||
collectionBinder.setTargetEntity( reflectionManager.toXClass( targetElement ) );
|
collectionBinder.setTargetEntity( reflectionManager.toXClass( targetElement ) );
|
||||||
//collectionBinder.setCascadeStrategy( getCascadeStrategy( embeddedCollectionAnn.cascade(), hibernateCascade ) );
|
//collectionBinder.setCascadeStrategy( getCascadeStrategy( embeddedCollectionAnn.cascade(), hibernateCascade ) );
|
||||||
//While this is a collection-valued property, its mapping significantly differs from those of one-to-many.
|
|
||||||
collectionBinder.setOneToMany( false );
|
collectionBinder.setOneToMany( false );
|
||||||
}
|
}
|
||||||
else if ( manyToManyAnn != null ) {
|
else if ( manyToManyAnn != null ) {
|
||||||
|
@ -1294,8 +1291,7 @@ public abstract class CollectionBinder {
|
||||||
&& !property.isAnnotationPresent( JoinColumns.class )) {
|
&& !property.isAnnotationPresent( JoinColumns.class )) {
|
||||||
throw new AnnotationException( "Unidirectional '@OneToMany' association '"
|
throw new AnnotationException( "Unidirectional '@OneToMany' association '"
|
||||||
+ qualify( propertyHolder.getPath(), propertyName )
|
+ qualify( propertyHolder.getPath(), propertyName )
|
||||||
+ "' is annotated '@OnDelete' and must explicitly specify a '@JoinColumn'"
|
+ "' is annotated '@OnDelete' and must explicitly specify a '@JoinColumn'" );
|
||||||
+ " (so that Join Table mechanic is not used)" );
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -16,7 +16,7 @@ import org.hibernate.engine.spi.EntityKey;
|
||||||
import org.hibernate.engine.spi.PersistenceContext;
|
import org.hibernate.engine.spi.PersistenceContext;
|
||||||
import org.hibernate.engine.spi.SessionFactoryImplementor;
|
import org.hibernate.engine.spi.SessionFactoryImplementor;
|
||||||
import org.hibernate.engine.spi.SessionImplementor;
|
import org.hibernate.engine.spi.SessionImplementor;
|
||||||
import org.hibernate.engine.spi.Status;
|
import org.hibernate.event.spi.EventSource;
|
||||||
import org.hibernate.internal.CoreMessageLogger;
|
import org.hibernate.internal.CoreMessageLogger;
|
||||||
import org.hibernate.persister.collection.CollectionPersister;
|
import org.hibernate.persister.collection.CollectionPersister;
|
||||||
import org.hibernate.pretty.MessageHelper;
|
import org.hibernate.pretty.MessageHelper;
|
||||||
|
@ -293,9 +293,28 @@ public final class Collections {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines if we can skip the explicit SQL delete statement, since
|
||||||
|
* the rows will be deleted by {@code on delete cascade}.
|
||||||
|
*/
|
||||||
|
public static boolean skipRemoval(EventSource session, CollectionPersister persister, Object key) {
|
||||||
|
if ( persister != null
|
||||||
|
// TODO: same optimization for @OneToMany @OnDelete(action=SET_NULL)
|
||||||
|
&& !persister.isOneToMany() && persister.isCascadeDeleteEnabled() ) {
|
||||||
|
final EntityKey entityKey = session.generateEntityKey( key, persister.getOwnerEntityPersister() );
|
||||||
|
final PersistenceContext persistenceContext = session.getPersistenceContextInternal();
|
||||||
|
final EntityEntry entry = persistenceContext.getEntry( persistenceContext.getEntity( entityKey ) );
|
||||||
|
return entry == null || entry.getStatus().isDeletedOrGone();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Disallow instantiation
|
* Disallow instantiation
|
||||||
*/
|
*/
|
||||||
private Collections() {
|
private Collections() {
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -39,6 +39,8 @@ import org.hibernate.persister.entity.EntityPersister;
|
||||||
|
|
||||||
import org.jboss.logging.Logger;
|
import org.jboss.logging.Logger;
|
||||||
|
|
||||||
|
import static org.hibernate.engine.internal.Collections.skipRemoval;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A convenience base class for listeners whose functionality results in flushing.
|
* A convenience base class for listeners whose functionality results in flushing.
|
||||||
*
|
*
|
||||||
|
@ -301,15 +303,17 @@ public abstract class AbstractFlushingEventListener implements JpaBootstrapSensi
|
||||||
}
|
}
|
||||||
if ( ce.isDoremove() ) {
|
if ( ce.isDoremove() ) {
|
||||||
interceptor.onCollectionRemove( coll, ce.getLoadedKey() );
|
interceptor.onCollectionRemove( coll, ce.getLoadedKey() );
|
||||||
actionQueue.addAction(
|
if ( !skipRemoval( session, ce.getLoadedPersister(), ce.getLoadedKey() ) ) {
|
||||||
new CollectionRemoveAction(
|
actionQueue.addAction(
|
||||||
coll,
|
new CollectionRemoveAction(
|
||||||
ce.getLoadedPersister(),
|
coll,
|
||||||
ce.getLoadedKey(),
|
ce.getLoadedPersister(),
|
||||||
ce.isSnapshotEmpty( coll ),
|
ce.getLoadedKey(),
|
||||||
session
|
ce.isSnapshotEmpty( coll ),
|
||||||
)
|
session
|
||||||
);
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if ( ce.isDoupdate() ) {
|
if ( ce.isDoupdate() ) {
|
||||||
interceptor.onCollectionUpdate( coll, ce.getLoadedKey() );
|
interceptor.onCollectionUpdate( coll, ce.getLoadedKey() );
|
||||||
|
|
|
@ -51,6 +51,8 @@ import org.hibernate.type.CompositeType;
|
||||||
import org.hibernate.type.Type;
|
import org.hibernate.type.Type;
|
||||||
import org.hibernate.type.TypeHelper;
|
import org.hibernate.type.TypeHelper;
|
||||||
|
|
||||||
|
import static org.hibernate.engine.internal.Collections.skipRemoval;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Defines the default delete event listener used by hibernate for deleting entities
|
* Defines the default delete event listener used by hibernate for deleting entities
|
||||||
* from the datastore in response to generated delete events.
|
* from the datastore in response to generated delete events.
|
||||||
|
@ -138,7 +140,7 @@ public class DefaultDeleteEventListener implements DeleteEventListener, Callback
|
||||||
if ( type.isCollectionType() ) {
|
if ( type.isCollectionType() ) {
|
||||||
final String role = ( (CollectionType) type ).getRole();
|
final String role = ( (CollectionType) type ).getRole();
|
||||||
final CollectionPersister persister = mappingMetamodel.getCollectionDescriptor(role);
|
final CollectionPersister persister = mappingMetamodel.getCollectionDescriptor(role);
|
||||||
if ( !persister.isInverse() ) {
|
if ( !persister.isInverse() && !skipRemoval( session, persister, key ) ) {
|
||||||
actionQueue.addAction( new CollectionRemoveAction( persister, key, session ) );
|
actionQueue.addAction( new CollectionRemoveAction( persister, key, session ) );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -136,8 +136,8 @@ public class WrapVisitor extends ProxyVisitor {
|
||||||
session
|
session
|
||||||
);
|
);
|
||||||
persistenceContext.addUninitializedCollection( persister, collectionInstance, key );
|
persistenceContext.addUninitializedCollection( persister, collectionInstance, key );
|
||||||
final CollectionEntry collectionEntry = persistenceContext.getCollectionEntry(
|
final CollectionEntry collectionEntry =
|
||||||
collectionInstance );
|
persistenceContext.getCollectionEntry( collectionInstance );
|
||||||
collectionEntry.setDoremove( true );
|
collectionEntry.setDoremove( true );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -206,6 +206,8 @@ public abstract class AbstractCollectionPersister
|
||||||
private final boolean hasOrphanDelete;
|
private final boolean hasOrphanDelete;
|
||||||
private final boolean subselectLoadable;
|
private final boolean subselectLoadable;
|
||||||
|
|
||||||
|
private final boolean cascadeDeleteEnabled;
|
||||||
|
|
||||||
// extra information about the element type
|
// extra information about the element type
|
||||||
private final Class<?> elementClass;
|
private final Class<?> elementClass;
|
||||||
|
|
||||||
|
@ -589,6 +591,9 @@ public abstract class AbstractCollectionPersister
|
||||||
}
|
}
|
||||||
|
|
||||||
tableMapping = buildCollectionTableMapping( collectionBootDescriptor, getTableName(), getCollectionSpaces() );
|
tableMapping = buildCollectionTableMapping( collectionBootDescriptor, getTableName(), getCollectionSpaces() );
|
||||||
|
|
||||||
|
cascadeDeleteEnabled = collectionBootDescriptor.getKey().isCascadeDeleteEnabled()
|
||||||
|
&& creationContext.getDialect().supportsCascadeDelete();
|
||||||
}
|
}
|
||||||
|
|
||||||
private BeforeExecutionGenerator createGenerator(RuntimeModelCreationContext context, IdentifierCollection collection) {
|
private BeforeExecutionGenerator createGenerator(RuntimeModelCreationContext context, IdentifierCollection collection) {
|
||||||
|
@ -1115,6 +1120,10 @@ public abstract class AbstractCollectionPersister
|
||||||
return isInverse;
|
return isInverse;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isCascadeDeleteEnabled() {
|
||||||
|
return cascadeDeleteEnabled;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getTableName() {
|
public String getTableName() {
|
||||||
return qualifiedTableName;
|
return qualifiedTableName;
|
||||||
|
|
|
@ -81,10 +81,6 @@ public class BasicCollectionPersister extends AbstractCollectionPersister {
|
||||||
private final DeleteRowsCoordinator deleteRowsCoordinator;
|
private final DeleteRowsCoordinator deleteRowsCoordinator;
|
||||||
private final RemoveCoordinator removeCoordinator;
|
private final RemoveCoordinator removeCoordinator;
|
||||||
|
|
||||||
public boolean isCascadeDeleteEnabled() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Deprecated(since = "6.0")
|
@Deprecated(since = "6.0")
|
||||||
public BasicCollectionPersister(
|
public BasicCollectionPersister(
|
||||||
Collection collectionBinding,
|
Collection collectionBinding,
|
||||||
|
|
|
@ -100,7 +100,6 @@ public class OneToManyPersister extends AbstractCollectionPersister {
|
||||||
private final DeleteRowsCoordinator deleteRowsCoordinator;
|
private final DeleteRowsCoordinator deleteRowsCoordinator;
|
||||||
private final RemoveCoordinator removeCoordinator;
|
private final RemoveCoordinator removeCoordinator;
|
||||||
|
|
||||||
private final boolean cascadeDeleteEnabled;
|
|
||||||
private final boolean keyIsNullable;
|
private final boolean keyIsNullable;
|
||||||
private final MutationExecutorService mutationExecutorService;
|
private final MutationExecutorService mutationExecutorService;
|
||||||
|
|
||||||
|
@ -117,8 +116,6 @@ public class OneToManyPersister extends AbstractCollectionPersister {
|
||||||
CollectionDataAccess cacheAccessStrategy,
|
CollectionDataAccess cacheAccessStrategy,
|
||||||
RuntimeModelCreationContext creationContext) throws MappingException, CacheException {
|
RuntimeModelCreationContext creationContext) throws MappingException, CacheException {
|
||||||
super( collectionBinding, cacheAccessStrategy, creationContext );
|
super( collectionBinding, cacheAccessStrategy, creationContext );
|
||||||
cascadeDeleteEnabled = collectionBinding.getKey().isCascadeDeleteEnabled()
|
|
||||||
&& creationContext.getDialect().supportsCascadeDelete();
|
|
||||||
keyIsNullable = collectionBinding.getKey().isNullable();
|
keyIsNullable = collectionBinding.getKey().isNullable();
|
||||||
|
|
||||||
this.rowMutationOperations = buildRowMutationOperations();
|
this.rowMutationOperations = buildRowMutationOperations();
|
||||||
|
@ -157,10 +154,6 @@ public class OneToManyPersister extends AbstractCollectionPersister {
|
||||||
return super.isRowDeleteEnabled() && keyIsNullable;
|
return super.isRowDeleteEnabled() && keyIsNullable;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isCascadeDeleteEnabled() {
|
|
||||||
return cascadeDeleteEnabled;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void recreate(PersistentCollection<?> collection, Object id, SharedSessionContractImplementor session)
|
public void recreate(PersistentCollection<?> collection, Object id, SharedSessionContractImplementor session)
|
||||||
throws HibernateException {
|
throws HibernateException {
|
||||||
|
|
|
@ -10,6 +10,7 @@ import org.hibernate.annotations.OnDelete;
|
||||||
import org.hibernate.annotations.OnDeleteAction;
|
import org.hibernate.annotations.OnDeleteAction;
|
||||||
import org.hibernate.exception.ConstraintViolationException;
|
import org.hibernate.exception.ConstraintViolationException;
|
||||||
|
|
||||||
|
import org.hibernate.stat.spi.StatisticsImplementor;
|
||||||
import org.hibernate.testing.orm.junit.DialectFeatureChecks;
|
import org.hibernate.testing.orm.junit.DialectFeatureChecks;
|
||||||
import org.hibernate.testing.orm.junit.DomainModel;
|
import org.hibernate.testing.orm.junit.DomainModel;
|
||||||
import org.hibernate.testing.orm.junit.ExpectedException;
|
import org.hibernate.testing.orm.junit.ExpectedException;
|
||||||
|
@ -28,6 +29,7 @@ import jakarta.persistence.GeneratedValue;
|
||||||
import jakarta.persistence.Id;
|
import jakarta.persistence.Id;
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
|
||||||
@DomainModel(
|
@DomainModel(
|
||||||
annotatedClasses = {
|
annotatedClasses = {
|
||||||
|
@ -36,7 +38,7 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||||
OnDeleteCascadeToElementCollectionTest.NonCascading.class
|
OnDeleteCascadeToElementCollectionTest.NonCascading.class
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@SessionFactory
|
@SessionFactory(generateStatistics = true)
|
||||||
@ExtendWith(ExpectedExceptionExtension.class)
|
@ExtendWith(ExpectedExceptionExtension.class)
|
||||||
@JiraKey("HHH-4301")
|
@JiraKey("HHH-4301")
|
||||||
public class OnDeleteCascadeToElementCollectionTest {
|
public class OnDeleteCascadeToElementCollectionTest {
|
||||||
|
@ -76,6 +78,99 @@ public class OnDeleteCascadeToElementCollectionTest {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@RequiresDialectFeature(feature = DialectFeatureChecks.SupportsCascadeDeleteCheck.class)
|
||||||
|
public void testCascadingDeleteUnloaded(SessionFactoryScope scope) {
|
||||||
|
var instance = new Cascading();
|
||||||
|
|
||||||
|
scope.inTransaction(
|
||||||
|
session -> {
|
||||||
|
instance.labels = new HashSet<>( Set.of( "one", "two" ) );
|
||||||
|
instance.tickets = new HashMap<>( Map.of(
|
||||||
|
"t1", new Ticket( "t1-2398", LocalDate.of( 2023, 8, 26 ) ),
|
||||||
|
"t2", new Ticket( "t2-23132", LocalDate.of( 2007, 9, 26 ) )
|
||||||
|
) );
|
||||||
|
session.persist( instance );
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
StatisticsImplementor statistics = scope.getSessionFactory().getStatistics();
|
||||||
|
statistics.clear();
|
||||||
|
|
||||||
|
scope.inTransaction(
|
||||||
|
session -> {
|
||||||
|
session.remove(session.getReference(instance));
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
assertEquals( 0, statistics.getEntityLoadCount() );
|
||||||
|
assertEquals( 1, statistics.getEntityDeleteCount() );
|
||||||
|
assertEquals( 0, statistics.getCollectionLoadCount() );
|
||||||
|
assertEquals( 0, statistics.getCollectionRemoveCount() );
|
||||||
|
|
||||||
|
scope.inTransaction(
|
||||||
|
session -> {
|
||||||
|
assertEquals( 0L,
|
||||||
|
session.createSelectionQuery( "from Cascading", Long.class )
|
||||||
|
.getResultCount() );
|
||||||
|
assertEquals( 0L,
|
||||||
|
session.createNativeQuery( "select count(*) from Cascading_tickets", Long.class )
|
||||||
|
.getSingleResult() );
|
||||||
|
assertEquals( 0L,
|
||||||
|
session.createNativeQuery( "select count(*) from Cascading_labels", Long.class )
|
||||||
|
.getSingleResult() );
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@RequiresDialectFeature(feature = DialectFeatureChecks.SupportsCascadeDeleteCheck.class)
|
||||||
|
public void testCascadingDeleteLoaded(SessionFactoryScope scope) {
|
||||||
|
var instance = new Cascading();
|
||||||
|
|
||||||
|
scope.inTransaction(
|
||||||
|
session -> {
|
||||||
|
instance.labels = new HashSet<>( Set.of( "one", "two" ) );
|
||||||
|
instance.tickets = new HashMap<>( Map.of(
|
||||||
|
"t1", new Ticket( "t1-2398", LocalDate.of( 2023, 8, 26 ) ),
|
||||||
|
"t2", new Ticket( "t2-23132", LocalDate.of( 2007, 9, 26 ) )
|
||||||
|
) );
|
||||||
|
session.persist( instance );
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
StatisticsImplementor statistics = scope.getSessionFactory().getStatistics();
|
||||||
|
statistics.clear();
|
||||||
|
|
||||||
|
scope.inTransaction(
|
||||||
|
session -> {
|
||||||
|
Cascading entity = session.find(Cascading.class, instance.id);
|
||||||
|
entity.labels.size();
|
||||||
|
entity.tickets.size();
|
||||||
|
session.remove( entity );
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
assertEquals( 1, statistics.getEntityLoadCount() );
|
||||||
|
assertEquals( 1, statistics.getEntityDeleteCount() );
|
||||||
|
assertEquals( 2, statistics.getCollectionLoadCount() );
|
||||||
|
assertEquals( 0, statistics.getCollectionRemoveCount() );
|
||||||
|
|
||||||
|
scope.inTransaction(
|
||||||
|
session -> {
|
||||||
|
assertEquals( 0L,
|
||||||
|
session.createSelectionQuery( "from Cascading", Long.class )
|
||||||
|
.getResultCount() );
|
||||||
|
assertEquals( 0L,
|
||||||
|
session.createNativeQuery( "select count(*) from Cascading_tickets", Long.class )
|
||||||
|
.getSingleResult() );
|
||||||
|
assertEquals( 0L,
|
||||||
|
session.createNativeQuery( "select count(*) from Cascading_labels", Long.class )
|
||||||
|
.getSingleResult() );
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ExpectedException(ConstraintViolationException.class)
|
@ExpectedException(ConstraintViolationException.class)
|
||||||
public void testNonCascading(SessionFactoryScope scope) {
|
public void testNonCascading(SessionFactoryScope scope) {
|
||||||
|
|
Loading…
Reference in New Issue