Skip to content

Commit 09fa8ef

Browse files
committed
HHH-18553 flush/evict when there is a managed instance while deleting the detached instance
Signed-off-by: Gavin King <[email protected]>
1 parent 5c89079 commit 09fa8ef

File tree

3 files changed

+70
-46
lines changed

3 files changed

+70
-46
lines changed

hibernate-core/src/main/java/org/hibernate/Session.java

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -597,7 +597,7 @@ public interface Session extends SharedSessionContract, EntityManager {
597597
* The modes {@link LockMode#WRITE} and {@link LockMode#UPGRADE_SKIPLOCKED}
598598
* are not legal arguments to {@code lock()}.
599599
*
600-
* @param object a persistent instance
600+
* @param object a persistent instance associated with this session
601601
* @param lockMode the lock level
602602
*/
603603
void lock(Object object, LockMode lockMode);
@@ -609,7 +609,7 @@ public interface Session extends SharedSessionContract, EntityManager {
609609
* This operation cascades to associated instances if the association is
610610
* mapped with {@link org.hibernate.annotations.CascadeType#LOCK}.
611611
*
612-
* @param object a persistent instance
612+
* @param object a persistent instance associated with this session
613613
* @param lockOptions the lock options
614614
*
615615
* @since 6.2
@@ -664,8 +664,12 @@ public interface Session extends SharedSessionContract, EntityManager {
664664
* Mark a persistence instance associated with this session for removal from
665665
* the underlying database. Ths operation cascades to associated instances if
666666
* the association is mapped {@link jakarta.persistence.CascadeType#REMOVE}.
667+
* <p>
668+
* Except when operating in fully JPA-compliant mode, this operation does,
669+
* contrary to the JPA specification, accept a detached entity instance.
667670
*
668-
* @param object the managed persistent instance to remove
671+
* @param object the managed persistent instance to remove, or a detached
672+
* instance unless operating in fully JPA-compliant mode
669673
*/
670674
@Override
671675
void remove(Object object);

hibernate-core/src/main/java/org/hibernate/event/internal/DefaultDeleteEventListener.java

Lines changed: 63 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import org.hibernate.CacheMode;
1010
import org.hibernate.HibernateException;
1111
import org.hibernate.LockMode;
12+
import org.hibernate.NonUniqueObjectException;
1213
import org.hibernate.TransientObjectException;
1314
import org.hibernate.action.internal.CollectionRemoveAction;
1415
import org.hibernate.action.internal.EntityDeleteAction;
@@ -44,14 +45,14 @@
4445
import org.hibernate.persister.entity.EntityPersister;
4546
import org.hibernate.pretty.MessageHelper;
4647
import org.hibernate.property.access.internal.PropertyAccessStrategyBackRefImpl;
47-
import org.hibernate.proxy.HibernateProxy;
4848
import org.hibernate.proxy.LazyInitializer;
4949
import org.hibernate.type.CollectionType;
5050
import org.hibernate.type.ComponentType;
5151
import org.hibernate.type.Type;
5252
import org.hibernate.type.TypeHelper;
5353

5454
import static org.hibernate.engine.internal.Collections.skipRemoval;
55+
import static org.hibernate.proxy.HibernateProxy.extractLazyInitializer;
5556

5657
/**
5758
* Defines the default delete event listener used by hibernate for deleting entities
@@ -102,7 +103,7 @@ public void onDelete(DeleteEvent event, DeleteContext transientEntities) throws
102103

103104
private boolean optimizeUnloadedDelete(DeleteEvent event) {
104105
final Object object = event.getObject();
105-
final LazyInitializer lazyInitializer = HibernateProxy.extractLazyInitializer( object );
106+
final LazyInitializer lazyInitializer = extractLazyInitializer( object );
106107
if ( lazyInitializer != null ) {
107108
if ( lazyInitializer.isUninitialized() ) {
108109
final EventSource source = event.getSession();
@@ -157,54 +158,76 @@ private void delete(DeleteEvent event, DeleteContext transientEntities) {
157158
final Object entity = persistenceContext.unproxyAndReassociate( event.getObject() );
158159
final EntityEntry entityEntry = persistenceContext.getEntry( entity );
159160
if ( entityEntry == null ) {
160-
deleteTransientInstance( event, transientEntities, entity );
161+
deleteUnmanagedInstance( event, transientEntities, entity );
161162
}
162163
else {
163164
deletePersistentInstance( event, transientEntities, entity, entityEntry );
164165
}
165166
}
166167

167-
private void deleteTransientInstance(DeleteEvent event, DeleteContext transientEntities, Object entity) {
168-
LOG.trace( "Entity was not persistent in delete processing" );
169-
168+
private void deleteUnmanagedInstance(DeleteEvent event, DeleteContext transientEntities, Object entity) {
169+
LOG.trace( "Deleted entity was not associated with current session" );
170170
final EventSource source = event.getSession();
171-
172171
final EntityPersister persister = source.getEntityPersister( event.getEntityName(), entity );
173172
if ( ForeignKeys.isTransient( persister.getEntityName(), entity, null, source ) ) {
174173
deleteTransientEntity( source, entity, persister, transientEntities );
175174
}
176175
else {
177176
performDetachedEntityDeletionCheck( event );
177+
deleteDetachedEntity( event, transientEntities, entity, persister, source );
178+
}
179+
}
178180

179-
final Object id = persister.getIdentifier( entity, source );
180-
if ( id == null ) {
181-
throw new TransientObjectException( "Cannot delete instance of entity '" + persister.getEntityName()
182-
+ "' because it has a null identifier" );
183-
}
181+
private void deleteDetachedEntity(DeleteEvent event, DeleteContext transientEntities, Object entity, EntityPersister persister, EventSource source) {
182+
final Object id = persister.getIdentifier( entity, source );
183+
if ( id == null ) {
184+
throw new TransientObjectException( "Cannot delete instance of entity '"
185+
+ persister.getEntityName() + "' because it has a null identifier" );
186+
}
184187

185-
final PersistenceContext persistenceContext = source.getPersistenceContextInternal();
186-
final EntityKey key = source.generateEntityKey( id, persister );
188+
final PersistenceContext persistenceContext = source.getPersistenceContextInternal();
189+
final EntityKey key = source.generateEntityKey( id, persister);
190+
final Object version = persister.getVersion(entity);
187191

188-
persistenceContext.checkUniqueness( key, entity);
192+
// persistenceContext.checkUniqueness( key, entity );
193+
flushAndEvictExistingEntity( key, version, persister, source );
189194

190-
new OnUpdateVisitor( source, id, entity ).process( entity, persister );
195+
new OnUpdateVisitor( source, id, entity ).process( entity, persister );
191196

192-
final Object version = persister.getVersion( entity );
197+
final EntityEntry entityEntry = persistenceContext.addEntity(
198+
entity,
199+
persister.isMutable() ? Status.MANAGED : Status.READ_ONLY,
200+
persister.getValues(entity),
201+
key,
202+
version,
203+
LockMode.NONE,
204+
true,
205+
persister,
206+
false
207+
);
208+
persister.afterReassociate(entity, source);
193209

194-
final EntityEntry entityEntry = persistenceContext.addEntity(
195-
entity,
196-
persister.isMutable() ? Status.MANAGED : Status.READ_ONLY,
197-
persister.getValues( entity ),
198-
key,
199-
version,
200-
LockMode.NONE,
201-
true,
202-
persister,
203-
false
204-
);
205-
persister.afterReassociate( entity, source );
210+
delete( event, transientEntities, source, entity, persister, id, version, entityEntry );
211+
}
206212

207-
delete( event, transientEntities, source, entity, persister, id, version, entityEntry );
213+
/**
214+
* Since Hibernate 7, if a detached instance is passed to remove(),
215+
* and if there is already an existing managed entity with the same
216+
* id, flush and evict it, after checking that the versions match.
217+
*/
218+
private static void flushAndEvictExistingEntity(
219+
EntityKey key, Object version, EntityPersister persister, EventSource source) {
220+
final Object existingEntity = source.getPersistenceContextInternal().getEntity( key );
221+
if ( existingEntity != null ) {
222+
source.flush();
223+
if ( !persister.isVersioned()
224+
|| persister.getVersionType()
225+
.isEqual( version, persister.getVersion( existingEntity ) ) ) {
226+
source.evict( existingEntity );
227+
}
228+
else {
229+
throw new NonUniqueObjectException( key.getIdentifier(), key.getEntityName() );
230+
}
208231
}
209232
}
210233

@@ -287,33 +310,31 @@ private static boolean hasCustomEventListeners(EventSource source) {
287310
}
288311

289312
private boolean hasRegisteredRemoveCallbacks(EntityPersister persister) {
290-
Class<?> mappedClass = persister.getMappedClass();
313+
final Class<?> mappedClass = persister.getMappedClass();
291314
return callbackRegistry.hasRegisteredCallbacks( mappedClass, CallbackType.PRE_REMOVE )
292315
|| callbackRegistry.hasRegisteredCallbacks( mappedClass, CallbackType.POST_REMOVE );
293316
}
294317

295318
/**
296319
* Called when we have recognized an attempt to delete a detached entity.
297320
* <p>
298-
* This is perfectly valid in Hibernate usage; JPA, however, forbids this.
299-
* Thus, this is a hook for HEM to affect this behavior.
300-
*
301-
* @param event The event.
321+
* This is perfectly legal in regular Hibernate usage; the JPA spec,
322+
* however, forbids it.
302323
*/
303324
protected void performDetachedEntityDeletionCheck(DeleteEvent event) {
304325
if ( jpaBootstrap ) {
305326
disallowDeletionOfDetached( event );
306327
}
307-
// ok in normal Hibernate usage to delete a detached entity; JPA however
308-
// forbids it, thus this is a hook for HEM to affect this behavior
309328
}
310329

311330
private void disallowDeletionOfDetached(DeleteEvent event) {
312-
EventSource source = event.getSession();
313-
String entityName = event.getEntityName();
314-
EntityPersister persister = source.getEntityPersister( entityName, event.getObject() );
315-
Object id = persister.getIdentifier( event.getObject(), source );
316-
entityName = entityName == null ? source.guessEntityName( event.getObject() ) : entityName;
331+
final EventSource source = event.getSession();
332+
final String explicitEntityName = event.getEntityName();
333+
final EntityPersister persister = source.getEntityPersister( explicitEntityName, event.getObject() );
334+
final Object id = persister.getIdentifier( event.getObject(), source );
335+
final String entityName = explicitEntityName == null
336+
? source.guessEntityName( event.getObject() )
337+
: explicitEntityName;
317338
throw new IllegalArgumentException( "Given entity is not associated with the persistence context [" + entityName + "#" + id + "]" );
318339
}
319340

hibernate-core/src/test/java/org/hibernate/orm/test/readonly/ReadOnlyProxyTest.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
import org.hibernate.Hibernate;
1313
import org.hibernate.Session;
1414
import org.hibernate.Transaction;
15-
import org.hibernate.TransientObjectException;
1615
import org.hibernate.UnresolvableObjectException;
1716
import org.hibernate.cfg.AvailableSettings;
1817
import org.hibernate.engine.spi.SessionImplementor;

0 commit comments

Comments
 (0)