HHH-17634 Merging a new entity having a @GeneratedValue id should not set the generated id of the original entity

This commit is contained in:
Andrea Boriero 2024-01-10 16:02:04 +01:00 committed by Christian Beikov
parent 7751cc4491
commit 186bcc6ac8
1 changed files with 113 additions and 40 deletions

View File

@ -25,6 +25,7 @@ import org.hibernate.engine.spi.EntityKey;
import org.hibernate.engine.spi.PersistenceContext; import org.hibernate.engine.spi.PersistenceContext;
import org.hibernate.engine.spi.PersistentAttributeInterceptor; import org.hibernate.engine.spi.PersistentAttributeInterceptor;
import org.hibernate.engine.spi.SelfDirtinessTracker; import org.hibernate.engine.spi.SelfDirtinessTracker;
import org.hibernate.engine.spi.SessionFactoryImplementor;
import org.hibernate.engine.spi.SessionImplementor; import org.hibernate.engine.spi.SessionImplementor;
import org.hibernate.event.spi.EntityCopyObserver; import org.hibernate.event.spi.EntityCopyObserver;
import org.hibernate.event.spi.EventSource; import org.hibernate.event.spi.EventSource;
@ -40,8 +41,10 @@ import org.hibernate.proxy.HibernateProxy;
import org.hibernate.proxy.LazyInitializer; import org.hibernate.proxy.LazyInitializer;
import org.hibernate.stat.spi.StatisticsImplementor; import org.hibernate.stat.spi.StatisticsImplementor;
import org.hibernate.type.CollectionType; import org.hibernate.type.CollectionType;
import org.hibernate.type.CompositeType;
import org.hibernate.type.EntityType; import org.hibernate.type.EntityType;
import org.hibernate.type.ForeignKeyDirection; import org.hibernate.type.ForeignKeyDirection;
import org.hibernate.type.Type;
import org.hibernate.type.TypeHelper; import org.hibernate.type.TypeHelper;
import static org.hibernate.engine.internal.ManagedTypeHelper.asPersistentAttributeInterceptable; import static org.hibernate.engine.internal.ManagedTypeHelper.asPersistentAttributeInterceptable;
@ -49,6 +52,7 @@ import static org.hibernate.engine.internal.ManagedTypeHelper.asSelfDirtinessTra
import static org.hibernate.engine.internal.ManagedTypeHelper.isHibernateProxy; import static org.hibernate.engine.internal.ManagedTypeHelper.isHibernateProxy;
import static org.hibernate.engine.internal.ManagedTypeHelper.isPersistentAttributeInterceptable; import static org.hibernate.engine.internal.ManagedTypeHelper.isPersistentAttributeInterceptable;
import static org.hibernate.engine.internal.ManagedTypeHelper.isSelfDirtinessTracker; import static org.hibernate.engine.internal.ManagedTypeHelper.isSelfDirtinessTracker;
import static org.hibernate.event.internal.EntityState.getEntityState;
/** /**
* Defines the default copy event listener used by hibernate for copying entities * Defines the default copy event listener used by hibernate for copying entities
@ -144,15 +148,70 @@ public class DefaultMergeEventListener
} }
private void merge(MergeEvent event, MergeContext copiedAlready, Object entity) { private void merge(MergeEvent event, MergeContext copiedAlready, Object entity) {
switch ( entityState( event, entity ) ) { final EventSource source = event.getSession();
// Check the persistence context for an entry relating to this
// entity to be merged...
final PersistenceContext persistenceContext = source.getPersistenceContextInternal();
EntityEntry entry = persistenceContext.getEntry( entity );
final EntityState entityState;
final Object copiedId;
final Object originalId;
if ( entry == null ) {
final EntityPersister persister = source.getEntityPersister( event.getEntityName(), entity );
originalId = persister.getIdentifier( entity, source );
if ( originalId != null ) {
final EntityKey entityKey;
if ( persister.getIdentifierType().isComponentType() ) {
/*
this is needed in case of composite id containing an association with a generated identifier, in such a case
generating the EntityKey will cause a NPE when trying to get the hashcode of the null id
*/
copiedId = copyCompositeTypeId(
originalId,
(CompositeType) persister.getIdentifierType(),
source,
copiedAlready
);
entityKey = source.generateEntityKey( copiedId, persister );
}
else {
copiedId = null;
entityKey = source.generateEntityKey( originalId, persister );
}
final Object managedEntity = persistenceContext.getEntity( entityKey );
entry = persistenceContext.getEntry( managedEntity );
if ( entry != null ) {
// we have a special case of a detached entity from the
// perspective of the merge operation. Specifically, we have
// an incoming entity instance which has a corresponding
// entry in the current persistence context, but registered
// under a different entity instance
entityState = EntityState.DETACHED;
}
else {
entityState = getEntityState( entity, event.getEntityName(), entry, source, false );
}
}
else {
copiedId = null;
entityState = getEntityState( entity, event.getEntityName(), entry, source, false );
}
}
else {
copiedId = null;
originalId = null;
entityState = getEntityState( entity, event.getEntityName(), entry, source, false );
}
switch ( entityState ) {
case DETACHED: case DETACHED:
entityIsDetached(event, copiedAlready); entityIsDetached( event, copiedId, originalId, copiedAlready );
break; break;
case TRANSIENT: case TRANSIENT:
entityIsTransient(event, copiedAlready); entityIsTransient( event, copiedId != null ? copiedId : originalId, copiedAlready );
break; break;
case PERSISTENT: case PERSISTENT:
entityIsPersistent(event, copiedAlready); entityIsPersistent( event, copiedAlready );
break; break;
default: //DELETED default: //DELETED
if ( event.getSession().getPersistenceContext().getEntry( entity ) == null ) { if ( event.getSession().getPersistenceContext().getEntry( entity ) == null ) {
@ -165,7 +224,7 @@ public class DefaultMergeEventListener
) )
); );
event.getSession().getActionQueue().unScheduleUnloadedDeletion( entity ); event.getSession().getActionQueue().unScheduleUnloadedDeletion( entity );
entityIsDetached(event, copiedAlready); entityIsDetached(event, copiedId, originalId, copiedAlready);
break; break;
} }
throw new ObjectDeletedException( throw new ObjectDeletedException(
@ -176,30 +235,37 @@ public class DefaultMergeEventListener
} }
} }
private static EntityState entityState(MergeEvent event, Object entity) { private static Object copyCompositeTypeId(
final EventSource source = event.getSession(); Object id,
// Check the persistence context for an entry relating to this CompositeType compositeType,
// entity to be merged... EventSource session,
final PersistenceContext persistenceContext = source.getPersistenceContextInternal(); MergeContext mergeContext) {
EntityEntry entry = persistenceContext.getEntry( entity ); final SessionFactoryImplementor sessionFactory = session.getSessionFactory();
if ( entry == null ) { final Object idCopy = compositeType.deepCopy( id, sessionFactory );
EntityPersister persister = source.getEntityPersister( event.getEntityName(), entity ); final Type[] subtypes = compositeType.getSubtypes();
Object id = persister.getIdentifier( entity, source ); final Object[] propertyValues = compositeType.getPropertyValues( id );
if ( id != null ) { final Object[] copyValues = compositeType.getPropertyValues( idCopy );
final EntityKey entityKey = source.generateEntityKey( id, persister ); for ( int i = 0; i < subtypes.length; i++ ) {
final Object managedEntity = persistenceContext.getEntity( entityKey ); final Type subtype = subtypes[i];
entry = persistenceContext.getEntry( managedEntity ); if ( subtype.isEntityType() ) {
if ( entry != null ) { // the value of the copy in the MergeContext has the id assigned
// we have a special case of a detached entity from the final Object o = mergeContext.get( propertyValues[i] );
// perspective of the merge operation. Specifically, we have if ( o != null ) {
// an incoming entity instance which has a corresponding copyValues[i] = o;
// entry in the current persistence context, but registered }
// under a different entity instance else {
return EntityState.DETACHED; copyValues[i] = subtype.deepCopy( propertyValues[i], sessionFactory );
} }
} }
else if ( subtype.isComponentType() ) {
copyValues[i] = copyCompositeTypeId( propertyValues[i], (CompositeType) subtype, session, mergeContext );
} }
return EntityState.getEntityState( entity, event.getEntityName(), entry, source, false ); else {
copyValues[i] = subtype.deepCopy( propertyValues[i], sessionFactory );
}
}
compositeType.setPropertyValues( idCopy, copyValues );
return idCopy;
} }
protected void entityIsPersistent(MergeEvent event, MergeContext copyCache) { protected void entityIsPersistent(MergeEvent event, MergeContext copyCache) {
@ -214,14 +280,13 @@ public class DefaultMergeEventListener
event.setResult( entity ); event.setResult( entity );
} }
protected void entityIsTransient(MergeEvent event, MergeContext copyCache) { protected void entityIsTransient(MergeEvent event, Object id, MergeContext copyCache) {
LOG.trace( "Merging transient instance" ); LOG.trace( "Merging transient instance" );
final Object entity = event.getEntity(); final Object entity = event.getEntity();
final EventSource session = event.getSession(); final EventSource session = event.getSession();
final String entityName = event.getEntityName(); final String entityName = event.getEntityName();
final EntityPersister persister = session.getEntityPersister( entityName, entity ); final EntityPersister persister = session.getEntityPersister( entityName, entity );
final Object id = persister.getIdentifier( entity, session );
final Object copy = copyEntity( copyCache, entity, session, persister, id ); final Object copy = copyEntity( copyCache, entity, session, persister, id );
// cascade first, so that all unsaved objects get their // cascade first, so that all unsaved objects get their
@ -231,7 +296,6 @@ public class DefaultMergeEventListener
copyValues( persister, entity, copy, session, copyCache, ForeignKeyDirection.FROM_PARENT ); copyValues( persister, entity, copy, session, copyCache, ForeignKeyDirection.FROM_PARENT );
saveTransientEntity( copy, entityName, event.getRequestedId(), session, copyCache ); saveTransientEntity( copy, entityName, event.getRequestedId(), session, copyCache );
persister.setIdentifier( entity, persister.getIdentifier( copy, session ), session );
// cascade first, so that all unsaved objects get their // cascade first, so that all unsaved objects get their
// copy created before we actually copy // copy created before we actually copy
@ -318,17 +382,25 @@ public class DefaultMergeEventListener
} }
} }
protected void entityIsDetached(MergeEvent event, MergeContext copyCache) { protected void entityIsDetached(MergeEvent event, Object copiedId, Object originalId, MergeContext copyCache) {
LOG.trace( "Merging detached instance" ); LOG.trace( "Merging detached instance" );
final Object entity = event.getEntity(); final Object entity = event.getEntity();
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 );
final String entityName = persister.getEntityName(); final String entityName = persister.getEntityName();
if ( originalId == null ) {
Object id = getDetachedEntityId( event, entity, persister ); originalId = persister.getIdentifier( entity, source );
}
final Object clonedIdentifier;
if ( copiedId == null ) {
clonedIdentifier = persister.getIdentifierType().deepCopy( originalId, source.getFactory() );
}
else {
clonedIdentifier = copiedId;
}
final Object id = getDetachedEntityId( event, originalId, persister );
// we must clone embedded composite identifiers, or we will get back the same instance that we pass in // we must clone embedded composite identifiers, or we will get back the same instance that we pass in
final Object clonedIdentifier = persister.getIdentifierType().deepCopy( id, source.getFactory() );
// apply the special MERGE fetch profile and perform the resolution (Session#get) // apply the special MERGE fetch profile and perform the resolution (Session#get)
final Object result = source.getLoadQueryInfluencers().fromInternalFetchProfile( final Object result = source.getLoadQueryInfluencers().fromInternalFetchProfile(
CascadingFetchProfile.MERGE, CascadingFetchProfile.MERGE,
@ -343,7 +415,7 @@ public class DefaultMergeEventListener
// we got here because we assumed that an instance // we got here because we assumed that an instance
// with an assigned id was detached, when it was // with an assigned id was detached, when it was
// really persistent // really persistent
entityIsTransient( event, copyCache ); entityIsTransient( event, clonedIdentifier, copyCache );
} }
else { else {
// before cascade! // before cascade!
@ -357,7 +429,6 @@ public class DefaultMergeEventListener
markInterceptorDirty( entity, target ); markInterceptorDirty( entity, target );
event.setResult( result ); event.setResult( result );
} }
} }
private static Object targetEntity(MergeEvent event, Object entity, EntityPersister persister, Object id, Object result) { private static Object targetEntity(MergeEvent event, Object entity, EntityPersister persister, Object id, Object result) {
@ -386,15 +457,15 @@ public class DefaultMergeEventListener
} }
} }
private static Object getDetachedEntityId(MergeEvent event, Object entity, EntityPersister persister) { private static Object getDetachedEntityId(MergeEvent event, Object originalId, EntityPersister persister) {
final EventSource source = event.getSession(); final EventSource source = event.getSession();
final Object id = event.getRequestedId(); final Object id = event.getRequestedId();
if ( id == null ) { if ( id == null ) {
return persister.getIdentifier( entity, source ); return originalId;
} }
else { else {
// check that entity id = requestedId // check that entity id = requestedId
Object entityId = persister.getIdentifier( entity, source ); final Object entityId = originalId;
if ( !persister.getIdentifierType().isEqual( id, entityId, source.getFactory() ) ) { if ( !persister.getIdentifierType().isEqual( id, entityId, source.getFactory() ) ) {
throw new HibernateException( "merge requested with id not matching id of passed entity" ); throw new HibernateException( "merge requested with id not matching id of passed entity" );
} }
@ -414,8 +485,10 @@ public class DefaultMergeEventListener
if ( isPersistentAttributeInterceptable( incoming ) if ( isPersistentAttributeInterceptable( incoming )
&& persister.getBytecodeEnhancementMetadata().isEnhancedForLazyLoading() ) { && persister.getBytecodeEnhancementMetadata().isEnhancedForLazyLoading() ) {
final PersistentAttributeInterceptor incomingInterceptor = asPersistentAttributeInterceptable( incoming ).$$_hibernate_getInterceptor(); final PersistentAttributeInterceptor incomingInterceptor =
final PersistentAttributeInterceptor managedInterceptor = asPersistentAttributeInterceptable( managed ).$$_hibernate_getInterceptor(); asPersistentAttributeInterceptable( incoming ).$$_hibernate_getInterceptor();
final PersistentAttributeInterceptor managedInterceptor =
asPersistentAttributeInterceptable( managed ).$$_hibernate_getInterceptor();
// todo - do we need to specially handle the case where both `incoming` and `managed` are initialized, but // todo - do we need to specially handle the case where both `incoming` and `managed` are initialized, but
// with different attributes initialized? // with different attributes initialized?