Skip to content

HHH-19535 Interceptor and merge() #10316

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Jun 13, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 42 additions & 3 deletions hibernate-core/src/main/java/org/hibernate/Interceptor.java
Original file line number Diff line number Diff line change
Expand Up @@ -378,7 +378,7 @@
* @param entity The entity instance being deleted
* @param id The identifier of the entity
* @param state The entity state
* @param propertyNames The names of the entity properties.
* @param propertyNames The names of the entity properties
* @param propertyTypes The types of the entity properties
*
* @see StatelessSession#update(Object)
Expand All @@ -391,7 +391,7 @@
* @param entity The entity instance being deleted
* @param id The identifier of the entity
* @param state The entity state
* @param propertyNames The names of the entity properties.
* @param propertyNames The names of the entity properties
* @param propertyTypes The types of the entity properties
*
* @see StatelessSession#upsert(String, Object)
Expand All @@ -403,10 +403,49 @@
*
* @param entity The entity instance being deleted
* @param id The identifier of the entity
* @param propertyNames The names of the entity properties.
* @param propertyNames The names of the entity properties
* @param propertyTypes The types of the entity properties
*
* @see StatelessSession#delete(Object)
*/
default void onDelete(Object entity, Object id, String[] propertyNames, Type[] propertyTypes) {}

/**
* Called before copying the state of a merged entity to a managed entity
* belonging to the persistence context of a stateful {@link Session}.
* <p>
* The interceptor may modify the {@code state}.
*
* @param entity The entity passed to {@code merge()}
* @param state The state of the entity passed to {@code merge()}
* @param propertyNames The names of the entity properties
* @param propertyTypes The types of the entity properties
*
* @since 7.1
*/
@Incubating
default void preMerge(Object entity, Object[] state, String[] propertyNames, Type[] propertyTypes) {}

Check notice

Code scanning / CodeQL

Useless parameter Note

The parameter 'entity' is never used.

Check notice

Code scanning / CodeQL

Useless parameter Note

The parameter 'propertyNames' is never used.

/**
* Called after copying the state of a merged entity to a managed entity
* belonging to the persistence context of a stateful {@link Session}.
* <p>
* Modification of the {@code sourceState} or {@code targetState} has no effect.
*
* @param source The entity passed to {@code merge()}
* @param target The target managed entity
* @param id The identifier of the managed entity
* @param targetState The copied state already assigned to the target managed entity
* @param originalState The original state of the target managed entity before assignment of the copied state,
* or {@code null} if the target entity is a new instance
* @param propertyNames The names of the entity properties
* @param propertyTypes The types of the entity properties
*
* @since 7.1
*/
@Incubating
default void postMerge(
Object source, Object target, Object id,

Check notice

Code scanning / CodeQL

Useless parameter Note

The parameter 'source' is never used.

Check notice

Code scanning / CodeQL

Useless parameter Note

The parameter 'target' is never used.
Object[] targetState, Object[] originalState,
String[] propertyNames, Type[] propertyTypes) {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -157,13 +157,10 @@ public <T> T resolveStrategy(
return strategy.cast( strategyReference );
}

final Class<? extends T> implementationClass;
if ( strategyReference instanceof Class ) {
implementationClass = (Class<T>) strategyReference;
}
else {
implementationClass = selectStrategyImplementor( strategy, strategyReference.toString() );
}
final Class<? extends T> implementationClass =
strategyReference instanceof Class
? (Class<? extends T>) strategyReference
: selectStrategyImplementor( strategy, strategyReference.toString() );

try {
return creator.create( implementationClass );
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

import org.hibernate.AssertionFailure;
import org.hibernate.HibernateException;
import org.hibernate.Interceptor;
import org.hibernate.ObjectDeletedException;
import org.hibernate.StaleObjectStateException;
import org.hibernate.WrongClassException;
Expand All @@ -25,7 +26,6 @@
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;
import org.hibernate.event.spi.MergeContext;
Expand Down Expand Up @@ -220,7 +220,7 @@ private void merge(MergeEvent event, MergeContext copiedAlready, Object entity)
)
);
source.getActionQueue().unScheduleUnloadedDeletion( entity );
entityIsDetached(event, copiedId, originalId, copiedAlready);
entityIsDetached( event, copiedId, originalId, copiedAlready );
break;
}
throw new ObjectDeletedException( "deleted instance passed to merge",
Expand Down Expand Up @@ -274,7 +274,7 @@ protected void entityIsPersistent(MergeEvent event, MergeContext copyCache) {
final EntityPersister persister = source.getEntityPersister( event.getEntityName(), entity );
copyCache.put( entity, entity, true ); //before cascade!
cascadeOnMerge( source, persister, entity, copyCache );
copyValues( persister, entity, entity, source, copyCache );
TypeHelper.replace( persister, entity, source, entity, copyCache );
event.setResult( entity );
}

Expand All @@ -283,27 +283,55 @@ protected void entityIsTransient(MergeEvent event, Object id, MergeContext copyC

final Object entity = event.getEntity();
final EventSource session = event.getSession();
final Interceptor interceptor = session.getInterceptor();
final String entityName = event.getEntityName();
final EntityPersister persister = session.getEntityPersister( entityName, entity );
final String[] propertyNames = persister.getPropertyNames();
final Type[] propertyTypes = persister.getPropertyTypes();
final Object copy = copyEntity( copyCache, entity, session, persister, id );

// cascade first, so that all unsaved objects get their
// copy created before we actually copy
//cascadeOnMerge(event, persister, entity, copyCache, Cascades.CASCADE_BEFORE_MERGE);
super.cascadeBeforeSave( session, persister, entity, copyCache );
copyValues( persister, entity, copy, session, copyCache, ForeignKeyDirection.FROM_PARENT );

final Object[] sourceValues = persister.getValues( entity );
interceptor.preMerge( entity, sourceValues, propertyNames, propertyTypes );
final Object[] copiedValues = TypeHelper.replace(
sourceValues,
persister.getValues( copy ),
propertyTypes,
session,
copy,
copyCache,
ForeignKeyDirection.FROM_PARENT
);
persister.setValues( copy, copiedValues );

saveTransientEntity( copy, entityName, event.getRequestedId(), session, copyCache );

// cascade first, so that all unsaved objects get their
// copy created before we actually copy
super.cascadeAfterSave( session, persister, entity, copyCache );


copyValues( persister, entity, copy, session, copyCache, ForeignKeyDirection.TO_PARENT );
// this is the second pass through on a merge op, so here we limit the
// replacement to association types (value types were already replaced
// during the first pass)
// final Object[] newSourceValues = persister.getValues( entity );
final Object[] targetValues = TypeHelper.replaceAssociations(
sourceValues, // newSourceValues,
persister.getValues( copy ),
propertyTypes,
session,
copy,
copyCache,
ForeignKeyDirection.TO_PARENT
);
persister.setValues( copy, targetValues );
interceptor.postMerge( entity, copy, id, targetValues, null, propertyNames, propertyTypes );

// saveTransientEntity has been called using a copy that contains empty collections
// (copyValues uses `ForeignKeyDirection.FROM_PARENT`) then the PC may contain a wrong
// (copyValues uses ForeignKeyDirection.FROM_PARENT) then the PC may contain a wrong
// collection snapshot, the CollectionVisitor realigns the collection snapshot values
// with the final copy
new CollectionVisitor( copy, id, session )
Expand All @@ -315,9 +343,9 @@ protected void entityIsTransient(MergeEvent event, Object id, MergeContext copyC
event.setResult( copy );

if ( isPersistentAttributeInterceptable( copy ) ) {
final PersistentAttributeInterceptor interceptor =
final PersistentAttributeInterceptor attributeInterceptor =
asPersistentAttributeInterceptable( copy ).$$_hibernate_getInterceptor();
if ( interceptor == null ) {
if ( attributeInterceptor == null ) {
persister.getBytecodeEnhancementMetadata().injectInterceptor( copy, id, session );
}
}
Expand Down Expand Up @@ -383,29 +411,29 @@ protected void entityIsDetached(MergeEvent event, Object copiedId, Object origin
LOG.trace( "Merging detached instance" );

final Object entity = event.getEntity();
final EventSource source = event.getSession();
final EntityPersister persister = source.getEntityPersister( event.getEntityName(), entity );
final EventSource session = event.getSession();
final EntityPersister persister = session.getEntityPersister( event.getEntityName(), entity );
final String entityName = persister.getEntityName();
if ( originalId == null ) {
originalId = persister.getIdentifier( entity, source );
originalId = persister.getIdentifier( entity, session );
}
final Object clonedIdentifier = copiedId == null
? persister.getIdentifierType().deepCopy( originalId, event.getFactory() )
: 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
// apply the special MERGE fetch profile and perform the resolution (Session#get)
final Object result = source.getLoadQueryInfluencers().fromInternalFetchProfile(
final Object result = session.getLoadQueryInfluencers().fromInternalFetchProfile(
CascadingFetchProfile.MERGE,
() -> source.get( entityName, clonedIdentifier )
() -> session.get( entityName, clonedIdentifier )
);

if ( result == null ) {
LOG.trace( "Detached instance not found in database" );
// we got here because we assumed that an instance
// with an assigned id and no version was detached,
// when it was really transient (or deleted)
final Boolean knownTransient = persister.isTransient( entity, source );
final Boolean knownTransient = persister.isTransient( entity, session );
if ( knownTransient == Boolean.FALSE ) {
// we know for sure it's detached (generated id
// or a version property), and so the instance
Expand All @@ -425,8 +453,25 @@ protected void entityIsDetached(MergeEvent event, Object copiedId, Object origin
final Object target = targetEntity( event, entity, persister, id, result );
// cascade first, so that all unsaved objects get their
// copy created before we actually copy
cascadeOnMerge( source, persister, entity, copyCache );
copyValues( persister, entity, target, source, copyCache );
cascadeOnMerge( session, persister, entity, copyCache );

final Interceptor interceptor = session.getInterceptor();
final String[] propertyNames = persister.getPropertyNames();
final Type[] propertyTypes = persister.getPropertyTypes();

final Object[] sourceValues = persister.getValues( entity );
final Object[] originalValues = persister.getValues( target );
interceptor.preMerge( entity, sourceValues, propertyNames, propertyTypes );
final Object[] targetValues = TypeHelper.replace(
sourceValues,
originalValues,
propertyTypes,
session,
target,
copyCache
);
persister.setValues( target, targetValues );
interceptor.postMerge( entity, target, id, targetValues, originalValues, propertyNames, propertyTypes );
//copyValues works by reflection, so explicitly mark the entity instance dirty
markInterceptorDirty( entity, target );
event.setResult( result );
Expand Down Expand Up @@ -571,64 +616,6 @@ private static boolean existsInDatabase(Object entity, EventSource source, Entit
return entry != null && entry.isExistsInDatabase();
}

protected void copyValues(
final EntityPersister persister,
final Object entity,
final Object target,
final SessionImplementor source,
final MergeContext copyCache) {
if ( entity == target ) {
TypeHelper.replace( persister, entity, source, entity, copyCache );
}
else {
final Object[] copiedValues = TypeHelper.replace(
persister.getValues( entity ),
persister.getValues( target ),
persister.getPropertyTypes(),
source,
target,
copyCache
);
persister.setValues( target, copiedValues );
}
}

protected void copyValues(
final EntityPersister persister,
final Object entity,
final Object target,
final SessionImplementor source,
final MergeContext copyCache,
final ForeignKeyDirection foreignKeyDirection) {
final Object[] copiedValues;
if ( foreignKeyDirection == ForeignKeyDirection.TO_PARENT ) {
// this is the second pass through on a merge op, so here we limit the
// replacement to associations types (value types were already replaced
// during the first pass)
copiedValues = TypeHelper.replaceAssociations(
persister.getValues( entity ),
persister.getValues( target ),
persister.getPropertyTypes(),
source,
target,
copyCache,
foreignKeyDirection
);
}
else {
copiedValues = TypeHelper.replace(
persister.getValues( entity ),
persister.getValues( target ),
persister.getPropertyTypes(),
source,
target,
copyCache,
foreignKeyDirection
);
}
persister.setValues( target, copiedValues );
}

/**
* Perform any cascades needed as part of this copy event.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,12 +45,9 @@ public void entityCopyDetected(
}

private String getManagedOrDetachedEntityString(Object managedEntity, Object entity ) {
if ( entity == managedEntity) {
return "Managed: [" + entity + "]";
}
else {
return "Detached: [" + entity + "]";
}
return entity == managedEntity
? "Managed: [" + entity + "]"
: "Detached: [" + entity + "]";
}

public void clear() {
Expand Down
Loading
Loading