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.PersistentAttributeInterceptor;
import org.hibernate.engine.spi.SelfDirtinessTracker;
import org.hibernate.engine.spi.SessionFactoryImplementor;
import org.hibernate.engine.spi.SessionImplementor;
import org.hibernate.event.spi.EntityCopyObserver;
import org.hibernate.event.spi.EventSource;
@ -40,8 +41,10 @@ import org.hibernate.proxy.HibernateProxy;
import org.hibernate.proxy.LazyInitializer;
import org.hibernate.stat.spi.StatisticsImplementor;
import org.hibernate.type.CollectionType;
import org.hibernate.type.CompositeType;
import org.hibernate.type.EntityType;
import org.hibernate.type.ForeignKeyDirection;
import org.hibernate.type.Type;
import org.hibernate.type.TypeHelper;
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.isPersistentAttributeInterceptable;
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
@ -144,15 +148,70 @@ public class DefaultMergeEventListener
}
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:
entityIsDetached(event, copiedAlready);
entityIsDetached( event, copiedId, originalId, copiedAlready );
break;
case TRANSIENT:
entityIsTransient(event, copiedAlready);
entityIsTransient( event, copiedId != null ? copiedId : originalId, copiedAlready );
break;
case PERSISTENT:
entityIsPersistent(event, copiedAlready);
entityIsPersistent( event, copiedAlready );
break;
default: //DELETED
if ( event.getSession().getPersistenceContext().getEntry( entity ) == null ) {
@ -165,7 +224,7 @@ public class DefaultMergeEventListener
)
);
event.getSession().getActionQueue().unScheduleUnloadedDeletion( entity );
entityIsDetached(event, copiedAlready);
entityIsDetached(event, copiedId, originalId, copiedAlready);
break;
}
throw new ObjectDeletedException(
@ -176,30 +235,37 @@ public class DefaultMergeEventListener
}
}
private static EntityState entityState(MergeEvent event, Object 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 );
if ( entry == null ) {
EntityPersister persister = source.getEntityPersister( event.getEntityName(), entity );
Object id = persister.getIdentifier( entity, source );
if ( id != null ) {
final EntityKey entityKey = source.generateEntityKey( id, 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
return EntityState.DETACHED;
private static Object copyCompositeTypeId(
Object id,
CompositeType compositeType,
EventSource session,
MergeContext mergeContext) {
final SessionFactoryImplementor sessionFactory = session.getSessionFactory();
final Object idCopy = compositeType.deepCopy( id, sessionFactory );
final Type[] subtypes = compositeType.getSubtypes();
final Object[] propertyValues = compositeType.getPropertyValues( id );
final Object[] copyValues = compositeType.getPropertyValues( idCopy );
for ( int i = 0; i < subtypes.length; i++ ) {
final Type subtype = subtypes[i];
if ( subtype.isEntityType() ) {
// the value of the copy in the MergeContext has the id assigned
final Object o = mergeContext.get( propertyValues[i] );
if ( o != null ) {
copyValues[i] = o;
}
else {
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) {
@ -214,14 +280,13 @@ public class DefaultMergeEventListener
event.setResult( entity );
}
protected void entityIsTransient(MergeEvent event, MergeContext copyCache) {
protected void entityIsTransient(MergeEvent event, Object id, MergeContext copyCache) {
LOG.trace( "Merging transient instance" );
final Object entity = event.getEntity();
final EventSource session = event.getSession();
final String entityName = event.getEntityName();
final EntityPersister persister = session.getEntityPersister( entityName, entity );
final Object id = persister.getIdentifier( entity, session );
final Object copy = copyEntity( copyCache, entity, session, persister, id );
// 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 );
saveTransientEntity( copy, entityName, event.getRequestedId(), session, copyCache );
persister.setIdentifier( entity, persister.getIdentifier( copy, session ), session );
// cascade first, so that all unsaved objects get their
// 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" );
final Object entity = event.getEntity();
final EventSource source = event.getSession();
final EntityPersister persister = source.getEntityPersister( event.getEntityName(), entity );
final String entityName = persister.getEntityName();
Object id = getDetachedEntityId( event, entity, persister );
if ( originalId == null ) {
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
final Object clonedIdentifier = persister.getIdentifierType().deepCopy( id, source.getFactory() );
// apply the special MERGE fetch profile and perform the resolution (Session#get)
final Object result = source.getLoadQueryInfluencers().fromInternalFetchProfile(
CascadingFetchProfile.MERGE,
@ -343,7 +415,7 @@ public class DefaultMergeEventListener
// we got here because we assumed that an instance
// with an assigned id was detached, when it was
// really persistent
entityIsTransient( event, copyCache );
entityIsTransient( event, clonedIdentifier, copyCache );
}
else {
// before cascade!
@ -357,7 +429,6 @@ public class DefaultMergeEventListener
markInterceptorDirty( entity, target );
event.setResult( 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 Object id = event.getRequestedId();
if ( id == null ) {
return persister.getIdentifier( entity, source );
return originalId;
}
else {
// check that entity id = requestedId
Object entityId = persister.getIdentifier( entity, source );
final Object entityId = originalId;
if ( !persister.getIdentifierType().isEqual( id, entityId, source.getFactory() ) ) {
throw new HibernateException( "merge requested with id not matching id of passed entity" );
}
@ -414,8 +485,10 @@ public class DefaultMergeEventListener
if ( isPersistentAttributeInterceptable( incoming )
&& persister.getBytecodeEnhancementMetadata().isEnhancedForLazyLoading() ) {
final PersistentAttributeInterceptor incomingInterceptor = asPersistentAttributeInterceptable( incoming ).$$_hibernate_getInterceptor();
final PersistentAttributeInterceptor managedInterceptor = asPersistentAttributeInterceptable( managed ).$$_hibernate_getInterceptor();
final PersistentAttributeInterceptor incomingInterceptor =
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
// with different attributes initialized?