HHH-15509 enable unloaded-proxy delete for entities with owned collections

This commit is contained in:
Gavin King 2022-09-26 16:11:28 +02:00
parent 17e8b727e9
commit 49a2b20d76
6 changed files with 106 additions and 38 deletions

View File

@ -29,7 +29,7 @@ public final class CollectionRemoveAction extends CollectionAction {
*
* Use this constructor when the collection is non-null.
*
* @param collection The collection to to remove; must be non-null
* @param collection The collection to remove; must be non-null
* @param persister The collection's persister
* @param id The collection key
* @param emptySnapshot Indicates if the snapshot is empty
@ -81,6 +81,23 @@ public final class CollectionRemoveAction extends CollectionAction {
this.affectedOwner = affectedOwner;
}
/**
* Removes a persistent collection for an unloaded proxy.
*
* Use this constructor when the owning entity is has not been loaded.
* @param persister The collection's persister
* @param id The collection key
* @param session The session
*/
public CollectionRemoveAction(
final CollectionPersister persister,
final Object id,
final SharedSessionContractImplementor session) {
super( persister, null, id, session );
emptySnapshot = false;
affectedOwner = null;
}
@Override
public void execute() throws HibernateException {
preRemove();
@ -88,11 +105,11 @@ public final class CollectionRemoveAction extends CollectionAction {
final SharedSessionContractImplementor session = getSession();
if ( !emptySnapshot ) {
// an existing collection that was either non-empty or uninitialized
// an existing collection that was either nonempty or uninitialized
// is replaced by null or a different collection
// (if the collection is uninitialized, hibernate has no way of
// (if the collection is uninitialized, Hibernate has no way of
// knowing if the collection is actually empty without querying the db)
getPersister().remove( getKey(), session);
getPersister().remove( getKey(), session );
}
final PersistentCollection<?> collection = getCollection();

View File

@ -39,7 +39,8 @@ public class EntityDeleteAction extends EntityAction {
/**
* Constructs an EntityDeleteAction.
* @param id The entity identifier
*
* @param id The entity identifier
* @param state The current (extracted) entity state
* @param version The current entity version
* @param instance The entity instance
@ -61,7 +62,6 @@ public class EntityDeleteAction extends EntityAction {
this.state = state;
NaturalIdMapping naturalIdMapping = persister.getNaturalIdMapping();
if ( naturalIdMapping != null ) {
naturalIdValues = session.getPersistenceContextInternal().getNaturalIdResolutions()
.removeLocalResolution(
@ -72,6 +72,23 @@ public class EntityDeleteAction extends EntityAction {
}
}
/**
* Constructs an EntityDeleteAction for an unloaded proxy.
*
* @param id The entity identifier
* @param persister The entity persister
* @param session The session
*/
public EntityDeleteAction(
final Object id,
final EntityPersister persister,
final SessionImplementor session) {
super( session, id, null, persister );
this.version = null;
this.isCascadeDeleteEnabled = false;
this.state = null;
}
public Object getVersion() {
return version;
}

View File

@ -11,6 +11,7 @@ import org.hibernate.EmptyInterceptor;
import org.hibernate.HibernateException;
import org.hibernate.LockMode;
import org.hibernate.TransientObjectException;
import org.hibernate.action.internal.CollectionRemoveAction;
import org.hibernate.action.internal.EntityDeleteAction;
import org.hibernate.action.internal.OrphanRemovalAction;
import org.hibernate.bytecode.enhance.spi.LazyPropertyInitializer;
@ -21,10 +22,12 @@ import org.hibernate.engine.internal.CascadePoint;
import org.hibernate.engine.internal.ForeignKeys;
import org.hibernate.engine.internal.Nullability;
import org.hibernate.engine.internal.Nullability.NullabilityCheckType;
import org.hibernate.engine.spi.ActionQueue;
import org.hibernate.engine.spi.CascadingActions;
import org.hibernate.engine.spi.EntityEntry;
import org.hibernate.engine.spi.EntityKey;
import org.hibernate.engine.spi.PersistenceContext;
import org.hibernate.engine.spi.SharedSessionContractImplementor;
import org.hibernate.engine.spi.Status;
import org.hibernate.event.service.spi.JpaBootstrapSensitive;
import org.hibernate.event.spi.DeleteContext;
@ -37,6 +40,7 @@ import org.hibernate.internal.FastSessionServices;
import org.hibernate.jpa.event.spi.CallbackRegistry;
import org.hibernate.jpa.event.spi.CallbackRegistryConsumer;
import org.hibernate.jpa.event.spi.CallbackType;
import org.hibernate.metamodel.spi.MappingMetamodelImplementor;
import org.hibernate.persister.collection.CollectionPersister;
import org.hibernate.persister.entity.EntityPersister;
import org.hibernate.pretty.MessageHelper;
@ -44,6 +48,7 @@ import org.hibernate.property.access.internal.PropertyAccessStrategyBackRefImpl;
import org.hibernate.proxy.HibernateProxy;
import org.hibernate.proxy.LazyInitializer;
import org.hibernate.type.CollectionType;
import org.hibernate.type.CompositeType;
import org.hibernate.type.Type;
import org.hibernate.type.TypeHelper;
@ -110,17 +115,15 @@ public class DefaultDeleteEventListener implements DeleteEventListener, Callback
persistenceContext.reassociateProxy( object, id );
if ( !persistenceContext.containsDeletedUnloadedEntityKey( key ) ) {
persistenceContext.registerDeletedUnloadedEntityKey( key );
source.getActionQueue().addAction(
new EntityDeleteAction(
id,
null,
null,
null,
persister,
false,
source
)
);
if ( persister.hasOwnedCollections() ) {
// we're deleting an unloaded proxy with collections
for ( Type type : persister.getPropertyTypes() ) { //TODO: when we enable this for subclasses use getSubclassPropertyTypeClosure()
deleteOwnedCollections( type, id, source, source.getActionQueue() );
}
}
source.getActionQueue().addAction( new EntityDeleteAction( id, persister, source ) );
}
return true;
}
@ -129,6 +132,23 @@ public class DefaultDeleteEventListener implements DeleteEventListener, Callback
return false;
}
private void deleteOwnedCollections(Type type, Object key, SharedSessionContractImplementor session, ActionQueue actionQueue) {
MappingMetamodelImplementor mappingMetamodel = session.getFactory().getMappingMetamodel();
if ( type.isCollectionType() ) {
String role = ( (CollectionType) type ).getRole();
CollectionPersister persister = mappingMetamodel.getCollectionDescriptor(role);
if ( !persister.isInverse() ) {
actionQueue.addAction( new CollectionRemoveAction( persister, key, session ) );
}
}
else if ( type.isComponentType() ) {
Type[] subtypes = ( (CompositeType) type ).getSubtypes();
for ( Type subtype : subtypes ) {
deleteOwnedCollections( subtype, key, session, actionQueue );
}
}
}
private void delete(DeleteEvent event, DeleteContext transientEntities) {
final PersistenceContext persistenceContext = event.getSession().getPersistenceContextInternal();
final Object entity = persistenceContext.unproxyAndReassociate( event.getObject() );
@ -252,9 +272,8 @@ public class DefaultDeleteEventListener implements DeleteEventListener, Callback
private boolean canBeDeletedWithoutLoading(EventSource source, EntityPersister persister) {
return source.getInterceptor() == EmptyInterceptor.INSTANCE
&& !persister.implementsLifecycle()
&& !persister.hasSubclasses()
&& !persister.hasSubclasses() //TODO: should be unnecessary, using EntityPersister.getSubclassPropertyTypeClosure(), etc
&& !persister.hasCascadeDelete()
&& !persister.hasOwnedCollections()
&& !persister.hasNaturalIdentifier()
&& !hasRegisteredRemoveCallbacks( persister )
&& !hasCustomEventListeners( source );

View File

@ -3820,7 +3820,7 @@ public abstract class AbstractEntityPersister
if ( entry == null && !isMutable() ) {
throw new IllegalStateException( "Updating immutable entity that is not in session yet" );
}
if ( ( entityMetamodel.isDynamicUpdate() && dirtyFields != null ) ) {
if ( entityMetamodel.isDynamicUpdate() && dirtyFields != null ) {
// We need to generate the UPDATE SQL when dynamic-update="true"
propsToUpdate = getPropertiesToUpdate( dirtyFields, hasDirtyCollection );
// don't need to check laziness (dirty checking algorithm handles that)
@ -4014,41 +4014,42 @@ public abstract class AbstractEntityPersister
@Override
public void delete(Object id, Object version, Object object, SharedSessionContractImplementor session)
throws HibernateException {
final int span = getTableSpan();
boolean isImpliedOptimisticLocking = !entityMetamodel.isVersioned() && isAllOrDirtyOptLocking()
&& object != null; // null object signals that we're deleting an unloaded proxy
Object[] loadedState = null;
if ( isImpliedOptimisticLocking ) {
// need to treat this as if it where optimistic-lock="all" (dirty does *not* make sense);
// first we need to locate the "loaded" state
//
// Note, it potentially could be a proxy, so doAfterTransactionCompletion the location the safe way...
final EntityKey key = session.generateEntityKey( id, this );
final PersistenceContext persistenceContext = session.getPersistenceContextInternal();
Object entity = persistenceContext.getEntity( key );
Object entity = persistenceContext.getEntity( session.generateEntityKey( id, this ) );
if ( entity != null ) {
EntityEntry entry = persistenceContext.getEntry( entity );
loadedState = entry.getLoadedState();
}
}
final String[] deleteStrings;
if ( isImpliedOptimisticLocking && loadedState != null ) {
// we need to utilize dynamic delete statements
deleteStrings = generateSQLDeleteStrings( loadedState );
}
else if (object!=null) {
// otherwise, utilize the static delete statements
deleteStrings = getSQLDeleteStrings();
}
else {
deleteStrings = getSQLDeleteNoVersionCheckStrings();
}
for ( int j = span - 1; j >= 0; j-- ) {
final String[] deleteStrings = getSQLDeleteStrings( object, isImpliedOptimisticLocking, loadedState );
for ( int j = getTableSpan() - 1; j >= 0; j-- ) {
delete( id, version, j, object, deleteStrings[j], session, loadedState );
}
}
private String[] getSQLDeleteStrings(Object object, boolean isImpliedOptimisticLocking, Object[] loadedState) {
if ( isImpliedOptimisticLocking && loadedState != null ) {
// we need to utilize dynamic delete statements
return generateSQLDeleteStrings(loadedState);
}
else if ( object != null ) {
// otherwise, utilize the static delete statements
return getSQLDeleteStrings();
}
else {
// deleting an unloaded proxy
return getSQLDeleteNoVersionCheckStrings();
}
}
protected boolean isAllOrDirtyOptLocking() {
@ -4069,7 +4070,7 @@ public abstract class AbstractEntityPersister
Type[] types = getPropertyTypes();
for ( int i = 0; i < entityMetamodel.getPropertySpan(); i++ ) {
if ( isPropertyOfTable( i, j ) && versionability[i] ) {
// this property belongs to the table and it is not specifically
// This property belongs to the table, and it's not explicitly
// excluded from optimistic locking by optimistic-lock="false"
String[] propertyColumnNames = getPropertyColumnNames( i );
boolean[] propertyNullness = types[i].toColumnNullness( loadedState[i], getFactory() );

View File

@ -26,6 +26,8 @@ public class DeleteUnloadedProxyTest {
Transaction tx = em.beginTransaction();
c.setParent(p);
p.getChildren().add(c);
p.getWords().add("hello");
p.getWords().add("world");
em.persist(p);
tx.commit();
} );
@ -54,6 +56,8 @@ public class DeleteUnloadedProxyTest {
Transaction tx = em.beginTransaction();
c.setParent(p);
p.getChildren().add(c);
p.getWords().add("hello");
p.getWords().add("world");
em.persist(p);
tx.commit();
} );

View File

@ -1,13 +1,16 @@
package org.hibernate.orm.test.deleteunloaded;
import jakarta.persistence.CascadeType;
import jakarta.persistence.ElementCollection;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import jakarta.persistence.OneToMany;
import jakarta.persistence.Version;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
@Entity
@ -21,10 +24,17 @@ public class Parent {
@OneToMany(mappedBy = "parent", cascade = CascadeType.PERSIST)
private Set<Child> children = new HashSet<>();
@ElementCollection
private List<String> words = new ArrayList<>();
public Set<Child> getChildren() {
return children;
}
public List<String> getWords() {
return words;
}
public long getId() {
return id;
}