diff --git a/hibernate-core/src/main/java/org/hibernate/Interceptor.java b/hibernate-core/src/main/java/org/hibernate/Interceptor.java
index 7f266d9d7053..098f0b830e38 100644
--- a/hibernate-core/src/main/java/org/hibernate/Interceptor.java
+++ b/hibernate-core/src/main/java/org/hibernate/Interceptor.java
@@ -378,7 +378,7 @@ default void onInsert(Object entity, Object id, Object[] state, String[] propert
* @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)
@@ -391,7 +391,7 @@ default void onUpdate(Object entity, Object id, Object[] state, String[] propert
* @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)
@@ -403,10 +403,49 @@ default void onUpsert(Object entity, Object id, Object[] state, String[] propert
*
* @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}.
+ *
+ * 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) {}
+
+ /**
+ * Called after copying the state of a merged entity to a managed entity
+ * belonging to the persistence context of a stateful {@link Session}.
+ *
+ * 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,
+ Object[] targetState, Object[] originalState,
+ String[] propertyNames, Type[] propertyTypes) {}
}
diff --git a/hibernate-core/src/main/java/org/hibernate/boot/registry/selector/internal/StrategySelectorImpl.java b/hibernate-core/src/main/java/org/hibernate/boot/registry/selector/internal/StrategySelectorImpl.java
index 6a91d8c0c227..3a8183d52013 100644
--- a/hibernate-core/src/main/java/org/hibernate/boot/registry/selector/internal/StrategySelectorImpl.java
+++ b/hibernate-core/src/main/java/org/hibernate/boot/registry/selector/internal/StrategySelectorImpl.java
@@ -157,13 +157,10 @@ public T resolveStrategy(
return strategy.cast( strategyReference );
}
- final Class extends T> implementationClass;
- if ( strategyReference instanceof Class ) {
- implementationClass = (Class) 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 );
diff --git a/hibernate-core/src/main/java/org/hibernate/event/internal/DefaultMergeEventListener.java b/hibernate-core/src/main/java/org/hibernate/event/internal/DefaultMergeEventListener.java
index 170479eb7641..c4e8d2170bb7 100644
--- a/hibernate-core/src/main/java/org/hibernate/event/internal/DefaultMergeEventListener.java
+++ b/hibernate-core/src/main/java/org/hibernate/event/internal/DefaultMergeEventListener.java
@@ -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;
@@ -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;
@@ -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",
@@ -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 );
}
@@ -283,15 +283,30 @@ 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 );
@@ -299,11 +314,24 @@ protected void entityIsTransient(MergeEvent event, Object id, MergeContext copyC
// 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 )
@@ -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 );
}
}
@@ -383,11 +411,11 @@ 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() )
@@ -395,9 +423,9 @@ protected void entityIsDetached(MergeEvent event, Object copiedId, Object origin
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 ) {
@@ -405,7 +433,7 @@ protected void entityIsDetached(MergeEvent event, Object copiedId, Object origin
// 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
@@ -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 );
@@ -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.
*
diff --git a/hibernate-core/src/main/java/org/hibernate/event/internal/EntityCopyNotAllowedObserver.java b/hibernate-core/src/main/java/org/hibernate/event/internal/EntityCopyNotAllowedObserver.java
index c63278fec08b..bd6678e66ecf 100644
--- a/hibernate-core/src/main/java/org/hibernate/event/internal/EntityCopyNotAllowedObserver.java
+++ b/hibernate-core/src/main/java/org/hibernate/event/internal/EntityCopyNotAllowedObserver.java
@@ -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() {
diff --git a/hibernate-core/src/main/java/org/hibernate/event/internal/EntityCopyObserverFactoryInitiator.java b/hibernate-core/src/main/java/org/hibernate/event/internal/EntityCopyObserverFactoryInitiator.java
index 06bc29912a11..3525918455c9 100644
--- a/hibernate-core/src/main/java/org/hibernate/event/internal/EntityCopyObserverFactoryInitiator.java
+++ b/hibernate-core/src/main/java/org/hibernate/event/internal/EntityCopyObserverFactoryInitiator.java
@@ -18,7 +18,7 @@
/**
* Looks for the configuration property {@value AvailableSettings#MERGE_ENTITY_COPY_OBSERVER} and registers
- * the matching {@link EntityCopyObserverFactory} based on the configuration value.
+ * the matching {@link EntityCopyObserverFactory} based on the configuration observerClass.
*
* For known implementations some optimisations are possible, such as reusing a singleton for the stateless
* implementations. When a user plugs in a custom {@link EntityCopyObserver} we take a defensive approach.
@@ -32,7 +32,10 @@ public class EntityCopyObserverFactoryInitiator implements StandardServiceInitia
@Override
public EntityCopyObserverFactory initiateService(final Map configurationValues, final ServiceRegistryImplementor registry) {
final Object value = getConfigurationValue( configurationValues );
- if ( value.equals( EntityCopyNotAllowedObserver.SHORT_NAME )
+ if ( value instanceof EntityCopyObserverFactory factory ) {
+ return factory;
+ }
+ else if ( value.equals( EntityCopyNotAllowedObserver.SHORT_NAME )
|| value.equals( EntityCopyNotAllowedObserver.class.getName() ) ) {
LOG.debugf( "Configured EntityCopyObserver strategy: %s", EntityCopyNotAllowedObserver.SHORT_NAME );
return EntityCopyNotAllowedObserver.FACTORY_OF_SELF;
@@ -48,15 +51,16 @@ else if ( value.equals( EntityCopyAllowedLoggedObserver.SHORT_NAME )
return EntityCopyAllowedLoggedObserver.FACTORY_OF_SELF;
}
else {
- //We load an "example instance" just to get its Class;
- //this might look excessive, but it also happens to test eagerly (at boot) that we can actually construct these
- //and that they are indeed of the right type.
+ // We load an "example instance" just to get its Class;
+ // this might look excessive, but it also happens to test eagerly
+ // (at boot) that we can actually construct these and that they
+ // are indeed of the right type.
final EntityCopyObserver exampleInstance =
registry.requireService( StrategySelector.class )
.resolveStrategy( EntityCopyObserver.class, value );
- final Class> observerType = exampleInstance.getClass();
- LOG.debugf( "Configured EntityCopyObserver is a custom implementation of type %s", observerType.getName() );
- return new EntityObserversFactoryFromClass( observerType );
+ final Class extends EntityCopyObserver> observerType = exampleInstance.getClass();
+ LOG.debugf( "Configured EntityCopyObserver is a custom implementation of type '%s'", observerType.getName() );
+ return new EntityCopyObserverFactoryFromClass( observerType );
}
}
@@ -78,23 +82,17 @@ public Class getServiceInitiated() {
return EntityCopyObserverFactory.class;
}
- private static class EntityObserversFactoryFromClass implements EntityCopyObserverFactory {
-
- private final Class> value;
-
- public EntityObserversFactoryFromClass(Class> value) {
- this.value = value;
- }
+ private record EntityCopyObserverFactoryFromClass(Class extends EntityCopyObserver> observerClass)
+ implements EntityCopyObserverFactory {
@Override
- public EntityCopyObserver createEntityCopyObserver() {
- try {
- return (EntityCopyObserver) value.newInstance();
- }
- catch (Exception e) {
- throw new HibernateException( "Could not instantiate class of type " + value.getName() );
+ public EntityCopyObserver createEntityCopyObserver() {
+ try {
+ return observerClass.newInstance();
+ }
+ catch (Exception e) {
+ throw new HibernateException( "Could not instantiate class of type " + observerClass.getName() );
+ }
}
}
- }
-
}
diff --git a/hibernate-core/src/main/java/org/hibernate/event/spi/EntityCopyObserver.java b/hibernate-core/src/main/java/org/hibernate/event/spi/EntityCopyObserver.java
index d19083c661d1..119af5afacdc 100644
--- a/hibernate-core/src/main/java/org/hibernate/event/spi/EntityCopyObserver.java
+++ b/hibernate-core/src/main/java/org/hibernate/event/spi/EntityCopyObserver.java
@@ -24,14 +24,14 @@ public interface EntityCopyObserver {
void entityCopyDetected(Object managedEntity, Object mergeEntity1, Object mergeEntity2, EventSource session);
/**
- * Called when the top-level merge operation is complete.
+ * Called when the toplevel merge operation is complete.
*
* @param session The session
*/
void topLevelMergeComplete(EventSource session);
/**
- * Called to clear any data stored in this EntityCopyObserver.
+ * Called to clear any data stored in this {@code EntityCopyObserver}.
*/
void clear();
}
diff --git a/hibernate-core/src/main/java/org/hibernate/event/spi/EntityCopyObserverFactory.java b/hibernate-core/src/main/java/org/hibernate/event/spi/EntityCopyObserverFactory.java
index f61ee1c8fd52..cb9d238133cb 100644
--- a/hibernate-core/src/main/java/org/hibernate/event/spi/EntityCopyObserverFactory.java
+++ b/hibernate-core/src/main/java/org/hibernate/event/spi/EntityCopyObserverFactory.java
@@ -6,6 +6,9 @@
import org.hibernate.service.Service;
+/**
+ * A {@linkplain Service service} which creates new instances of {@link EntityCopyObserver}.
+ */
@FunctionalInterface
public interface EntityCopyObserverFactory extends Service {
EntityCopyObserver createEntityCopyObserver();
diff --git a/hibernate-core/src/main/java/org/hibernate/event/spi/MergeContext.java b/hibernate-core/src/main/java/org/hibernate/event/spi/MergeContext.java
index 09a67d12d8b5..85b4bd0f00b2 100644
--- a/hibernate-core/src/main/java/org/hibernate/event/spi/MergeContext.java
+++ b/hibernate-core/src/main/java/org/hibernate/event/spi/MergeContext.java
@@ -275,7 +275,7 @@ public Object put(Object mergeEntity, Object managedEntity, boolean isOperatedOn
* @throws IllegalStateException if internal cross-references are out of sync,
*/
public void putAll(Map,?> map) {
- for ( Entry,?> entry : map.entrySet() ) {
+ for ( var entry : map.entrySet() ) {
put( entry.getKey(), entry.getValue() );
}
}
diff --git a/hibernate-core/src/main/java/org/hibernate/proxy/HibernateProxy.java b/hibernate-core/src/main/java/org/hibernate/proxy/HibernateProxy.java
index dc9301314cb7..d122bd569a89 100644
--- a/hibernate-core/src/main/java/org/hibernate/proxy/HibernateProxy.java
+++ b/hibernate-core/src/main/java/org/hibernate/proxy/HibernateProxy.java
@@ -3,6 +3,8 @@
* Copyright Red Hat Inc. and Hibernate Authors
*/
package org.hibernate.proxy;
+
+import java.io.Serial;
import java.io.Serializable;
import org.hibernate.Internal;
@@ -42,6 +44,7 @@ public interface HibernateProxy extends Serializable, PrimeAmongSecondarySuperty
*
* @return The serializable proxy replacement.
*/
+ @Serial
Object writeReplace();
/**
diff --git a/hibernate-core/src/main/java/org/hibernate/type/TypeHelper.java b/hibernate-core/src/main/java/org/hibernate/type/TypeHelper.java
index 14bfb7fbdd6b..b1f4f07fd8f3 100644
--- a/hibernate-core/src/main/java/org/hibernate/type/TypeHelper.java
+++ b/hibernate-core/src/main/java/org/hibernate/type/TypeHelper.java
@@ -72,7 +72,7 @@ public static Object[] replace(
final SharedSessionContractImplementor session,
final Object owner,
final Map