HHH-18553 flush/evict when there is a managed instance while deleting the detached instance

Signed-off-by: Gavin King <gavin@hibernate.org>
This commit is contained in:
Gavin King 2024-08-31 19:07:43 +02:00
parent 5c89079f2e
commit 09fa8ef76a
3 changed files with 75 additions and 51 deletions

View File

@ -597,7 +597,7 @@ public interface Session extends SharedSessionContract, EntityManager {
* The modes {@link LockMode#WRITE} and {@link LockMode#UPGRADE_SKIPLOCKED} * The modes {@link LockMode#WRITE} and {@link LockMode#UPGRADE_SKIPLOCKED}
* are not legal arguments to {@code lock()}. * are not legal arguments to {@code lock()}.
* *
* @param object a persistent instance * @param object a persistent instance associated with this session
* @param lockMode the lock level * @param lockMode the lock level
*/ */
void lock(Object object, LockMode lockMode); void lock(Object object, LockMode lockMode);
@ -609,7 +609,7 @@ public interface Session extends SharedSessionContract, EntityManager {
* This operation cascades to associated instances if the association is * This operation cascades to associated instances if the association is
* mapped with {@link org.hibernate.annotations.CascadeType#LOCK}. * mapped with {@link org.hibernate.annotations.CascadeType#LOCK}.
* *
* @param object a persistent instance * @param object a persistent instance associated with this session
* @param lockOptions the lock options * @param lockOptions the lock options
* *
* @since 6.2 * @since 6.2
@ -664,8 +664,12 @@ public interface Session extends SharedSessionContract, EntityManager {
* Mark a persistence instance associated with this session for removal from * Mark a persistence instance associated with this session for removal from
* the underlying database. Ths operation cascades to associated instances if * the underlying database. Ths operation cascades to associated instances if
* the association is mapped {@link jakarta.persistence.CascadeType#REMOVE}. * the association is mapped {@link jakarta.persistence.CascadeType#REMOVE}.
* <p>
* Except when operating in fully JPA-compliant mode, this operation does,
* contrary to the JPA specification, accept a detached entity instance.
* *
* @param object the managed persistent instance to remove * @param object the managed persistent instance to remove, or a detached
* instance unless operating in fully JPA-compliant mode
*/ */
@Override @Override
void remove(Object object); void remove(Object object);

View File

@ -9,6 +9,7 @@ package org.hibernate.event.internal;
import org.hibernate.CacheMode; import org.hibernate.CacheMode;
import org.hibernate.HibernateException; import org.hibernate.HibernateException;
import org.hibernate.LockMode; import org.hibernate.LockMode;
import org.hibernate.NonUniqueObjectException;
import org.hibernate.TransientObjectException; import org.hibernate.TransientObjectException;
import org.hibernate.action.internal.CollectionRemoveAction; import org.hibernate.action.internal.CollectionRemoveAction;
import org.hibernate.action.internal.EntityDeleteAction; import org.hibernate.action.internal.EntityDeleteAction;
@ -44,7 +45,6 @@ import org.hibernate.persister.collection.CollectionPersister;
import org.hibernate.persister.entity.EntityPersister; import org.hibernate.persister.entity.EntityPersister;
import org.hibernate.pretty.MessageHelper; import org.hibernate.pretty.MessageHelper;
import org.hibernate.property.access.internal.PropertyAccessStrategyBackRefImpl; import org.hibernate.property.access.internal.PropertyAccessStrategyBackRefImpl;
import org.hibernate.proxy.HibernateProxy;
import org.hibernate.proxy.LazyInitializer; import org.hibernate.proxy.LazyInitializer;
import org.hibernate.type.CollectionType; import org.hibernate.type.CollectionType;
import org.hibernate.type.ComponentType; import org.hibernate.type.ComponentType;
@ -52,6 +52,7 @@ import org.hibernate.type.Type;
import org.hibernate.type.TypeHelper; import org.hibernate.type.TypeHelper;
import static org.hibernate.engine.internal.Collections.skipRemoval; import static org.hibernate.engine.internal.Collections.skipRemoval;
import static org.hibernate.proxy.HibernateProxy.extractLazyInitializer;
/** /**
* Defines the default delete event listener used by hibernate for deleting entities * Defines the default delete event listener used by hibernate for deleting entities
@ -102,7 +103,7 @@ public class DefaultDeleteEventListener implements DeleteEventListener, Callback
private boolean optimizeUnloadedDelete(DeleteEvent event) { private boolean optimizeUnloadedDelete(DeleteEvent event) {
final Object object = event.getObject(); final Object object = event.getObject();
final LazyInitializer lazyInitializer = HibernateProxy.extractLazyInitializer( object ); final LazyInitializer lazyInitializer = extractLazyInitializer( object );
if ( lazyInitializer != null ) { if ( lazyInitializer != null ) {
if ( lazyInitializer.isUninitialized() ) { if ( lazyInitializer.isUninitialized() ) {
final EventSource source = event.getSession(); final EventSource source = event.getSession();
@ -157,54 +158,76 @@ public class DefaultDeleteEventListener implements DeleteEventListener, Callback
final Object entity = persistenceContext.unproxyAndReassociate( event.getObject() ); final Object entity = persistenceContext.unproxyAndReassociate( event.getObject() );
final EntityEntry entityEntry = persistenceContext.getEntry( entity ); final EntityEntry entityEntry = persistenceContext.getEntry( entity );
if ( entityEntry == null ) { if ( entityEntry == null ) {
deleteTransientInstance( event, transientEntities, entity ); deleteUnmanagedInstance( event, transientEntities, entity );
} }
else { else {
deletePersistentInstance( event, transientEntities, entity, entityEntry ); deletePersistentInstance( event, transientEntities, entity, entityEntry );
} }
} }
private void deleteTransientInstance(DeleteEvent event, DeleteContext transientEntities, Object entity) { private void deleteUnmanagedInstance(DeleteEvent event, DeleteContext transientEntities, Object entity) {
LOG.trace( "Entity was not persistent in delete processing" ); LOG.trace( "Deleted entity was not associated with current session" );
final EventSource source = event.getSession(); final EventSource source = event.getSession();
final EntityPersister persister = source.getEntityPersister( event.getEntityName(), entity ); final EntityPersister persister = source.getEntityPersister( event.getEntityName(), entity );
if ( ForeignKeys.isTransient( persister.getEntityName(), entity, null, source ) ) { if ( ForeignKeys.isTransient( persister.getEntityName(), entity, null, source ) ) {
deleteTransientEntity( source, entity, persister, transientEntities ); deleteTransientEntity( source, entity, persister, transientEntities );
} }
else { else {
performDetachedEntityDeletionCheck( event ); performDetachedEntityDeletionCheck( event );
deleteDetachedEntity( event, transientEntities, entity, persister, source );
}
}
final Object id = persister.getIdentifier( entity, source ); private void deleteDetachedEntity(DeleteEvent event, DeleteContext transientEntities, Object entity, EntityPersister persister, EventSource source) {
if ( id == null ) { final Object id = persister.getIdentifier( entity, source );
throw new TransientObjectException( "Cannot delete instance of entity '" + persister.getEntityName() if ( id == null ) {
+ "' because it has a null identifier" ); throw new TransientObjectException( "Cannot delete instance of entity '"
+ persister.getEntityName() + "' because it has a null identifier" );
}
final PersistenceContext persistenceContext = source.getPersistenceContextInternal();
final EntityKey key = source.generateEntityKey( id, persister);
final Object version = persister.getVersion(entity);
// persistenceContext.checkUniqueness( key, entity );
flushAndEvictExistingEntity( key, version, persister, source );
new OnUpdateVisitor( source, id, entity ).process( entity, persister );
final EntityEntry entityEntry = persistenceContext.addEntity(
entity,
persister.isMutable() ? Status.MANAGED : Status.READ_ONLY,
persister.getValues(entity),
key,
version,
LockMode.NONE,
true,
persister,
false
);
persister.afterReassociate(entity, source);
delete( event, transientEntities, source, entity, persister, id, version, entityEntry );
}
/**
* Since Hibernate 7, if a detached instance is passed to remove(),
* and if there is already an existing managed entity with the same
* id, flush and evict it, after checking that the versions match.
*/
private static void flushAndEvictExistingEntity(
EntityKey key, Object version, EntityPersister persister, EventSource source) {
final Object existingEntity = source.getPersistenceContextInternal().getEntity( key );
if ( existingEntity != null ) {
source.flush();
if ( !persister.isVersioned()
|| persister.getVersionType()
.isEqual( version, persister.getVersion( existingEntity ) ) ) {
source.evict( existingEntity );
}
else {
throw new NonUniqueObjectException( key.getIdentifier(), key.getEntityName() );
} }
final PersistenceContext persistenceContext = source.getPersistenceContextInternal();
final EntityKey key = source.generateEntityKey( id, persister );
persistenceContext.checkUniqueness( key, entity);
new OnUpdateVisitor( source, id, entity ).process( entity, persister );
final Object version = persister.getVersion( entity );
final EntityEntry entityEntry = persistenceContext.addEntity(
entity,
persister.isMutable() ? Status.MANAGED : Status.READ_ONLY,
persister.getValues( entity ),
key,
version,
LockMode.NONE,
true,
persister,
false
);
persister.afterReassociate( entity, source );
delete( event, transientEntities, source, entity, persister, id, version, entityEntry );
} }
} }
@ -287,7 +310,7 @@ public class DefaultDeleteEventListener implements DeleteEventListener, Callback
} }
private boolean hasRegisteredRemoveCallbacks(EntityPersister persister) { private boolean hasRegisteredRemoveCallbacks(EntityPersister persister) {
Class<?> mappedClass = persister.getMappedClass(); final Class<?> mappedClass = persister.getMappedClass();
return callbackRegistry.hasRegisteredCallbacks( mappedClass, CallbackType.PRE_REMOVE ) return callbackRegistry.hasRegisteredCallbacks( mappedClass, CallbackType.PRE_REMOVE )
|| callbackRegistry.hasRegisteredCallbacks( mappedClass, CallbackType.POST_REMOVE ); || callbackRegistry.hasRegisteredCallbacks( mappedClass, CallbackType.POST_REMOVE );
} }
@ -295,25 +318,23 @@ public class DefaultDeleteEventListener implements DeleteEventListener, Callback
/** /**
* Called when we have recognized an attempt to delete a detached entity. * Called when we have recognized an attempt to delete a detached entity.
* <p> * <p>
* This is perfectly valid in Hibernate usage; JPA, however, forbids this. * This is perfectly legal in regular Hibernate usage; the JPA spec,
* Thus, this is a hook for HEM to affect this behavior. * however, forbids it.
*
* @param event The event.
*/ */
protected void performDetachedEntityDeletionCheck(DeleteEvent event) { protected void performDetachedEntityDeletionCheck(DeleteEvent event) {
if ( jpaBootstrap ) { if ( jpaBootstrap ) {
disallowDeletionOfDetached( event ); disallowDeletionOfDetached( event );
} }
// ok in normal Hibernate usage to delete a detached entity; JPA however
// forbids it, thus this is a hook for HEM to affect this behavior
} }
private void disallowDeletionOfDetached(DeleteEvent event) { private void disallowDeletionOfDetached(DeleteEvent event) {
EventSource source = event.getSession(); final EventSource source = event.getSession();
String entityName = event.getEntityName(); final String explicitEntityName = event.getEntityName();
EntityPersister persister = source.getEntityPersister( entityName, event.getObject() ); final EntityPersister persister = source.getEntityPersister( explicitEntityName, event.getObject() );
Object id = persister.getIdentifier( event.getObject(), source ); final Object id = persister.getIdentifier( event.getObject(), source );
entityName = entityName == null ? source.guessEntityName( event.getObject() ) : entityName; final String entityName = explicitEntityName == null
? source.guessEntityName( event.getObject() )
: explicitEntityName;
throw new IllegalArgumentException( "Given entity is not associated with the persistence context [" + entityName + "#" + id + "]" ); throw new IllegalArgumentException( "Given entity is not associated with the persistence context [" + entityName + "#" + id + "]" );
} }

View File

@ -12,7 +12,6 @@ import org.hibernate.CacheMode;
import org.hibernate.Hibernate; import org.hibernate.Hibernate;
import org.hibernate.Session; import org.hibernate.Session;
import org.hibernate.Transaction; import org.hibernate.Transaction;
import org.hibernate.TransientObjectException;
import org.hibernate.UnresolvableObjectException; import org.hibernate.UnresolvableObjectException;
import org.hibernate.cfg.AvailableSettings; import org.hibernate.cfg.AvailableSettings;
import org.hibernate.engine.spi.SessionImplementor; import org.hibernate.engine.spi.SessionImplementor;