HHH-17761 Merging a bytecode enhanced entity with all properties set to null does not apply the update

This commit is contained in:
Andrea Boriero 2024-03-29 11:33:35 +01:00 committed by Steve Ebersole
parent 78d4cce17e
commit 1dc67a323f
14 changed files with 185 additions and 13 deletions

View File

@ -241,6 +241,14 @@ public class EnhancerImpl implements Enhancer {
EnhancerConstants.NEXT_SETTER_NAME
);
builder = addFieldWithGetterAndSetter(
builder,
boolean.class,
EnhancerConstants.USE_TRACKER_FIELD_NAME,
EnhancerConstants.USE_TRACKER_GETTER_NAME,
EnhancerConstants.USE_TRACKER_SETTER_NAME
);
builder = addInterceptorHandling( builder, managedCtClass );
if ( enhancementContext.doDirtyCheckingInline( managedCtClass ) ) {

View File

@ -182,6 +182,11 @@ public final class EnhancerConstants {
*/
public static final String TRACKER_COMPOSITE_CLEAR_OWNER = "$$_hibernate_clearOwner";
public static final String USE_TRACKER_FIELD_NAME = "$$_hibernate_useTracker";
public static final String USE_TRACKER_GETTER_NAME = "$$_hibernate_useTracker";
public static final String USE_TRACKER_SETTER_NAME = "$$_hibernate_setUseTracker";
private EnhancerConstants() {
}
}

View File

@ -17,6 +17,7 @@ import org.hibernate.bytecode.enhance.spi.interceptor.LazyAttributesMetadata;
import org.hibernate.bytecode.spi.BytecodeEnhancementMetadata;
import org.hibernate.bytecode.spi.NotInstrumentedException;
import org.hibernate.engine.spi.EntityKey;
import org.hibernate.engine.spi.ManagedEntity;
import org.hibernate.engine.spi.PersistenceContext;
import org.hibernate.engine.spi.PersistentAttributeInterceptable;
import org.hibernate.engine.spi.PersistentAttributeInterceptor;
@ -31,6 +32,7 @@ import org.checkerframework.checker.nullness.qual.Nullable;
import static org.hibernate.engine.internal.ManagedTypeHelper.asPersistentAttributeInterceptable;
import static org.hibernate.engine.internal.ManagedTypeHelper.isPersistentAttributeInterceptableType;
import static org.hibernate.engine.internal.ManagedTypeHelper.processIfManagedEntity;
import static org.hibernate.engine.internal.ManagedTypeHelper.processIfSelfDirtinessTracker;
/**
@ -153,6 +155,8 @@ public final class BytecodeEnhancementMetadataPojoImpl implements BytecodeEnhanc
// clear the fields that are marked as dirty in the dirtiness tracker
processIfSelfDirtinessTracker( entity, BytecodeEnhancementMetadataPojoImpl::clearDirtyAttributes );
processIfManagedEntity( entity, BytecodeEnhancementMetadataPojoImpl::useTracker );
// add the entity (proxy) instance to the PC
persistenceContext.addEnhancedProxy( entityKey, entity );
@ -188,6 +192,10 @@ public final class BytecodeEnhancementMetadataPojoImpl implements BytecodeEnhanc
entity.$$_hibernate_clearDirtyAttributes();
}
private static void useTracker(final ManagedEntity entity) {
entity.$$_hibernate_setUseTracker( true );
}
@Override
public LazyAttributeLoadingInterceptor injectInterceptor(
Object entity,

View File

@ -19,6 +19,7 @@ import org.hibernate.collection.spi.PersistentCollection;
import org.hibernate.engine.spi.EntityEntry;
import org.hibernate.engine.spi.EntityEntryExtraState;
import org.hibernate.engine.spi.EntityKey;
import org.hibernate.engine.spi.ManagedEntity;
import org.hibernate.engine.spi.PersistenceContext;
import org.hibernate.engine.spi.PersistentAttributeInterceptor;
import org.hibernate.engine.spi.SelfDirtinessTracker;
@ -35,11 +36,13 @@ import static org.hibernate.engine.internal.AbstractEntityEntry.BooleanState.IS_
import static org.hibernate.engine.internal.AbstractEntityEntry.EnumState.LOCK_MODE;
import static org.hibernate.engine.internal.AbstractEntityEntry.EnumState.PREVIOUS_STATUS;
import static org.hibernate.engine.internal.AbstractEntityEntry.EnumState.STATUS;
import static org.hibernate.engine.internal.ManagedTypeHelper.asManagedEntity;
import static org.hibernate.engine.internal.ManagedTypeHelper.asPersistentAttributeInterceptable;
import static org.hibernate.engine.internal.ManagedTypeHelper.asSelfDirtinessTracker;
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.engine.internal.ManagedTypeHelper.processIfManagedEntity;
import static org.hibernate.engine.internal.ManagedTypeHelper.processIfSelfDirtinessTracker;
import static org.hibernate.engine.spi.CachedNaturalIdValueSource.LOAD;
import static org.hibernate.engine.spi.Status.DELETED;
@ -279,6 +282,7 @@ public abstract class AbstractEntityEntry implements Serializable, EntityEntry {
}
processIfSelfDirtinessTracker( entity, AbstractEntityEntry::clearDirtyAttributes );
processIfManagedEntity( entity, AbstractEntityEntry::useTracker );
final SharedSessionContractImplementor session = getPersistenceContext().getSession();
session.getFactory().getCustomEntityDirtinessStrategy()
@ -289,6 +293,10 @@ public abstract class AbstractEntityEntry implements Serializable, EntityEntry {
entity.$$_hibernate_clearDirtyAttributes();
}
private static void useTracker(final ManagedEntity entity) {
entity.$$_hibernate_setUseTracker( true );
}
@Override
public void postDelete() {
setCompressedValue( PREVIOUS_STATUS, getStatus() );
@ -367,7 +375,8 @@ public abstract class AbstractEntityEntry implements Serializable, EntityEntry {
return uninitializedProxy
|| !persister.hasCollections()
&& !persister.hasMutableProperties()
&& !asSelfDirtinessTracker( entity ).$$_hibernate_hasDirtyAttributes();
&& !asSelfDirtinessTracker( entity ).$$_hibernate_hasDirtyAttributes()
&& asManagedEntity( entity ).$$_hibernate_useTracker();
}
else {
if ( isPersistentAttributeInterceptable( entity ) ) {

View File

@ -553,9 +553,11 @@ public class EntityEntryContext {
private EntityEntry entityEntry;
private ManagedEntity previous;
private ManagedEntity next;
private boolean useTracker;
public ManagedEntityImpl(Object entityInstance) {
this.entityInstance = entityInstance;
useTracker = false;
}
@Override
@ -583,6 +585,16 @@ public class EntityEntryContext {
this.next = next;
}
@Override
public void $$_hibernate_setUseTracker(boolean useTracker) {
this.useTracker = useTracker;
}
@Override
public boolean $$_hibernate_useTracker() {
return useTracker;
}
@Override
public ManagedEntity $$_hibernate_getPreviousManagedEntity() {
return previous;
@ -663,6 +675,16 @@ public class EntityEntryContext {
this.next = next;
}
@Override
public void $$_hibernate_setUseTracker(boolean useTracker) {
managedEntity.$$_hibernate_setUseTracker( useTracker );
}
@Override
public boolean $$_hibernate_useTracker() {
return managedEntity.$$_hibernate_useTracker();
}
/*
Check instance type of EntityEntry and if type is ImmutableEntityEntry, check to see if entity is referenced cached in the second level cache
*/

View File

@ -218,6 +218,16 @@ public final class ManagedTypeHelper {
}
}
public static void processIfManagedEntity(final Object entity, final ManagedEntityConsumer action) {
if ( entity instanceof PrimeAmongSecondarySupertypes ) {
final PrimeAmongSecondarySupertypes t = (PrimeAmongSecondarySupertypes) entity;
final ManagedEntity e = t.asManagedEntity();
if ( e != null ) {
action.accept( e );
}
}
}
// Not using Consumer<SelfDirtinessTracker> because of JDK-8180450:
// use a custom functional interface with explicit type.
@FunctionalInterface
@ -225,6 +235,11 @@ public final class ManagedTypeHelper {
void accept(SelfDirtinessTracker tracker);
}
@FunctionalInterface
public interface ManagedEntityConsumer {
void accept(ManagedEntity entity);
}
/**
* If the entity is implementing SelfDirtinessTracker, apply some action to it; this action should take
* a parameter of type T.

View File

@ -80,6 +80,45 @@ public interface ManagedEntity extends Managed {
*/
void $$_hibernate_setNextManagedEntity(ManagedEntity next);
/**
* Used to understand if the tracker can be used to detect dirty properties.
*
* E.g:
* @Entity
* class MyEntity{
* @Id Integer id
* String name
* }
*
* inSession (
* session -> {
* MyEntity entity = new MyEntity(1, "Poul");
* session.persist(entity);
* });
*
*
* inSession (
* session -> {
* MyEntity entity = new MyEntity(1, null);
* session.merge(entity);
* });
*
* Because the attribute `name` has been set to null the SelfDirtyTracker does not detect any change and
* so doesn't mark the attribute as dirty so the merge does not perform any update.
*
*
* @param useTracker true if the tracker can be used to detect dirty properties, false otherwise
*
*/
void $$_hibernate_setUseTracker(boolean useTracker);
/**
* Can the tracker be used to detect dirty attributes
*
* @return true if the tracker can be used to detect dirty properties, false otherwise
*/
boolean $$_hibernate_useTracker();
/**
* Special internal contract to optimize type checking
* @see PrimeAmongSecondarySupertypes

View File

@ -20,6 +20,7 @@ import org.hibernate.engine.spi.CascadingAction;
import org.hibernate.engine.spi.EntityEntry;
import org.hibernate.engine.spi.EntityEntryExtraState;
import org.hibernate.engine.spi.EntityKey;
import org.hibernate.engine.spi.ManagedEntity;
import org.hibernate.engine.spi.PersistenceContext;
import org.hibernate.engine.spi.SelfDirtinessTracker;
import org.hibernate.engine.spi.SessionImplementor;
@ -38,6 +39,7 @@ import org.hibernate.type.Type;
import org.hibernate.type.TypeHelper;
import static org.hibernate.engine.internal.ManagedTypeHelper.processIfSelfDirtinessTracker;
import static org.hibernate.engine.internal.ManagedTypeHelper.processIfManagedEntity;
import static org.hibernate.engine.internal.Versioning.getVersion;
import static org.hibernate.engine.internal.Versioning.seedVersion;
import static org.hibernate.generator.EventType.INSERT;
@ -196,6 +198,7 @@ public abstract class AbstractSaveEventListener<C>
callbackRegistry.preCreate( entity );
processIfSelfDirtinessTracker( entity, SelfDirtinessTracker::$$_hibernate_clearDirtyAttributes );
processIfManagedEntity( entity, (managedEntity) -> managedEntity.$$_hibernate_setUseTracker( true ) );
if ( persister.getGenerator() instanceof Assigned ) {
id = persister.getIdentifier( entity, source );

View File

@ -18,6 +18,7 @@ import org.hibernate.bytecode.enhance.spi.interceptor.EnhancementAsProxyLaziness
import org.hibernate.engine.internal.Nullability;
import org.hibernate.engine.internal.Versioning;
import org.hibernate.engine.spi.EntityEntry;
import org.hibernate.engine.spi.ManagedEntity;
import org.hibernate.engine.spi.PersistenceContext;
import org.hibernate.engine.spi.PersistentAttributeInterceptor;
import org.hibernate.engine.spi.SelfDirtinessTracker;
@ -41,11 +42,13 @@ import org.hibernate.stat.spi.StatisticsImplementor;
import org.hibernate.type.Type;
import static org.hibernate.bytecode.enhance.spi.LazyPropertyInitializer.UNFETCHED_PROPERTY;
import static org.hibernate.engine.internal.ManagedTypeHelper.asManagedEntity;
import static org.hibernate.engine.internal.ManagedTypeHelper.asPersistentAttributeInterceptable;
import static org.hibernate.engine.internal.ManagedTypeHelper.asSelfDirtinessTracker;
import static org.hibernate.engine.internal.ManagedTypeHelper.isPersistentAttributeInterceptable;
import static org.hibernate.engine.internal.ManagedTypeHelper.isSelfDirtinessTracker;
import static org.hibernate.engine.internal.ManagedTypeHelper.processIfSelfDirtinessTracker;
import static org.hibernate.engine.internal.ManagedTypeHelper.processIfManagedEntity;
import static org.hibernate.engine.internal.Versioning.getVersion;
import static org.hibernate.engine.internal.Versioning.incrementVersion;
import static org.hibernate.engine.internal.Versioning.setVersion;
@ -219,6 +222,7 @@ public class DefaultFlushEntityEventListener implements FlushEntityEventListener
else {
final Object entity = event.getEntity();
processIfSelfDirtinessTracker( entity, SelfDirtinessTracker::$$_hibernate_clearDirtyAttributes );
processIfManagedEntity( entity, DefaultFlushEntityEventListener::useTracker );
final EventSource source = event.getSession();
source.getFactory()
.getCustomEntityDirtinessStrategy()
@ -231,6 +235,10 @@ public class DefaultFlushEntityEventListener implements FlushEntityEventListener
}
}
private static void useTracker(final ManagedEntity entity) {
entity.$$_hibernate_setUseTracker( true );
}
private boolean scheduleUpdate(final FlushEntityEvent event) {
final EntityEntry entry = event.getEntityEntry();
final EventSource session = event.getSession();
@ -553,9 +561,10 @@ public class DefaultFlushEntityEventListener implements FlushEntityEventListener
}
else {
final Object entity = event.getEntity();
return isSelfDirtinessTracker( entity )
? getDirtyPropertiesFromSelfDirtinessTracker( asSelfDirtinessTracker( entity ), event )
: getDirtyPropertiesFromCustomEntityDirtinessStrategy( event );
if ( isSelfDirtinessTracker( entity ) && asManagedEntity( entity ).$$_hibernate_useTracker() ) {
return getDirtyPropertiesFromSelfDirtinessTracker( asSelfDirtinessTracker( entity ), event );
}
return getDirtyPropertiesFromCustomEntityDirtinessStrategy( event );
}
}
@ -595,18 +604,26 @@ public class DefaultFlushEntityEventListener implements FlushEntityEventListener
final EntityEntry entry = event.getEntityEntry();
final EntityPersister persister = entry.getPersister();
if ( tracker.$$_hibernate_hasDirtyAttributes() || persister.hasMutableProperties() ) {
return persister.resolveDirtyAttributeIndexes(
event.getPropertyValues(),
entry.getLoadedState(),
tracker.$$_hibernate_getDirtyAttributes(),
event.getSession()
);
return resolveDirtyAttributeIndex( tracker, event, persister, entry );
}
else {
return ArrayHelper.EMPTY_INT_ARRAY;
}
}
private static int[] resolveDirtyAttributeIndex(
SelfDirtinessTracker tracker,
FlushEntityEvent event,
EntityPersister persister,
EntityEntry entry) {
return persister.resolveDirtyAttributeIndexes(
event.getPropertyValues(),
entry.getLoadedState(),
tracker.$$_hibernate_getDirtyAttributes(),
event.getSession()
);
}
private static class DirtyCheckAttributeInfoImpl implements CustomEntityDirtinessStrategy.AttributeInformation {
private final FlushEntityEvent event;
private final EntityPersister persister;

View File

@ -22,6 +22,7 @@ import org.hibernate.engine.spi.CascadingActions;
import org.hibernate.engine.spi.CollectionEntry;
import org.hibernate.engine.spi.EntityEntry;
import org.hibernate.engine.spi.EntityKey;
import org.hibernate.engine.spi.ManagedEntity;
import org.hibernate.engine.spi.PersistenceContext;
import org.hibernate.engine.spi.PersistentAttributeInterceptor;
import org.hibernate.engine.spi.SelfDirtinessTracker;
@ -47,6 +48,7 @@ import org.hibernate.type.ForeignKeyDirection;
import org.hibernate.type.Type;
import org.hibernate.type.TypeHelper;
import static org.hibernate.engine.internal.ManagedTypeHelper.asManagedEntity;
import static org.hibernate.engine.internal.ManagedTypeHelper.asPersistentAttributeInterceptable;
import static org.hibernate.engine.internal.ManagedTypeHelper.asSelfDirtinessTracker;
import static org.hibernate.engine.internal.ManagedTypeHelper.isHibernateProxy;
@ -514,10 +516,18 @@ public class DefaultMergeEventListener
// for enhanced entities, copy over the dirty attributes
if ( isSelfDirtinessTracker( entity ) && isSelfDirtinessTracker( target ) ) {
// clear, because setting the embedded attributes dirties them
final ManagedEntity managedEntity = asManagedEntity( target );
final boolean useTracker = asManagedEntity( entity ).$$_hibernate_useTracker();
final SelfDirtinessTracker selfDirtinessTrackerTarget = asSelfDirtinessTracker( target );
selfDirtinessTrackerTarget.$$_hibernate_clearDirtyAttributes();
for ( String fieldName : asSelfDirtinessTracker( entity ).$$_hibernate_getDirtyAttributes() ) {
selfDirtinessTrackerTarget.$$_hibernate_trackChange( fieldName );
if ( !selfDirtinessTrackerTarget.$$_hibernate_hasDirtyAttributes() && !useTracker ) {
managedEntity.$$_hibernate_setUseTracker( false );
}
else {
managedEntity.$$_hibernate_setUseTracker( true );
selfDirtinessTrackerTarget.$$_hibernate_clearDirtyAttributes();
for ( String fieldName : asSelfDirtinessTracker( entity ).$$_hibernate_getDirtyAttributes() ) {
selfDirtinessTrackerTarget.$$_hibernate_trackChange( fieldName );
}
}
}
}

View File

@ -90,6 +90,7 @@ import org.hibernate.engine.spi.EntityEntryFactory;
import org.hibernate.engine.spi.EntityHolder;
import org.hibernate.engine.spi.EntityKey;
import org.hibernate.engine.spi.LoadQueryInfluencers;
import org.hibernate.engine.spi.ManagedEntity;
import org.hibernate.engine.spi.NaturalIdResolutions;
import org.hibernate.engine.spi.PersistenceContext;
import org.hibernate.engine.spi.PersistentAttributeInterceptable;
@ -304,6 +305,7 @@ import static java.util.Collections.emptyMap;
import static java.util.Collections.emptySet;
import static org.hibernate.engine.internal.ManagedTypeHelper.asPersistentAttributeInterceptable;
import static org.hibernate.engine.internal.ManagedTypeHelper.isPersistentAttributeInterceptable;
import static org.hibernate.engine.internal.ManagedTypeHelper.processIfManagedEntity;
import static org.hibernate.engine.internal.ManagedTypeHelper.processIfPersistentAttributeInterceptable;
import static org.hibernate.engine.internal.ManagedTypeHelper.processIfSelfDirtinessTracker;
import static org.hibernate.engine.internal.Versioning.isVersionIncrementRequired;
@ -4403,12 +4405,17 @@ public abstract class AbstractEntityPersister
// clear the fields that are marked as dirty in the dirtiness tracker
processIfSelfDirtinessTracker( entity, AbstractEntityPersister::clearDirtyAttributes );
processIfManagedEntity( entity, AbstractEntityPersister::useTracker );
}
private static void clearDirtyAttributes(final SelfDirtinessTracker entity) {
entity.$$_hibernate_clearDirtyAttributes();
}
private static void useTracker(final ManagedEntity entity) {
entity.$$_hibernate_setUseTracker( true );
}
@Override
public String[] getPropertyNames() {
return entityMetamodel.getPropertyNames();

View File

@ -59,4 +59,14 @@ public class SimpleEntity implements ManagedEntity {
public void $$_hibernate_setNextManagedEntity(ManagedEntity next) {
}
@Override
public void $$_hibernate_setUseTracker(boolean useTracker) {
}
@Override
public boolean $$_hibernate_useTracker() {
return false;
}
}

View File

@ -123,5 +123,15 @@ public class BasicSessionTest extends BaseCoreFunctionalTestCase {
public void $$_hibernate_setPreviousManagedEntity(ManagedEntity previous) {
this.previous = previous;
}
@Override
public void $$_hibernate_setUseTracker(boolean useTracker) {
}
@Override
public boolean $$_hibernate_useTracker() {
return false;
}
}
}

View File

@ -310,5 +310,14 @@ public class ByteCodeEnhancedImmutableReferenceCacheTest extends BaseCoreFunctio
this.previous = previous;
}
@Override
public void $$_hibernate_setUseTracker(boolean useTracker) {
}
@Override
public boolean $$_hibernate_useTracker() {
return false;
}
}
}