diff --git a/hibernate-core/src/main/java/org/hibernate/engine/internal/EntityEntryImpl.java b/hibernate-core/src/main/java/org/hibernate/engine/internal/EntityEntryImpl.java index 2362f6b5f6ea..580bb05f998c 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/internal/EntityEntryImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/internal/EntityEntryImpl.java @@ -310,6 +310,11 @@ public void postUpdate(Object entity, Object[] updatedState, Object nextVersion) public void postLoad(Object entity) { processIfSelfDirtinessTracker( entity, EntityEntryImpl::clearDirtyAttributes ); processIfManagedEntity( entity, EntityEntryImpl::useTracker ); + + if ( persister.isMutable() && persistenceContext.getSession() instanceof SessionImplementor session ) { + session.getFactory().getCustomEntityDirtinessStrategy() + .resetDirty( entity, persister, session ); + } } private static void clearDirtyAttributes(final SelfDirtinessTracker entity) { diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/dirtiness/CustomDirtinessStrategyTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/dirtiness/CustomDirtinessStrategyTest.java index ecb2cd608853..5cbaa9d8ba07 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/dirtiness/CustomDirtinessStrategyTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/dirtiness/CustomDirtinessStrategyTest.java @@ -60,7 +60,7 @@ public void testOnlyCustomStrategy() { assertEquals( 1, Strategy.INSTANCE.canDirtyCheckCount ); assertEquals( 1, Strategy.INSTANCE.isDirtyCount ); - assertEquals( 1, Strategy.INSTANCE.resetDirtyCount ); + assertEquals( 2, Strategy.INSTANCE.resetDirtyCount ); assertEquals( 1, Strategy.INSTANCE.findDirtyCount ); session = openSession(); @@ -94,7 +94,7 @@ public void testCustomStrategyWithFlushInterceptor() { // As we used an interceptor, the custom strategy should have been called twice to find dirty properties assertEquals( 1, Strategy.INSTANCE.canDirtyCheckCount ); assertEquals( 1, Strategy.INSTANCE.isDirtyCount ); - assertEquals( 1, Strategy.INSTANCE.resetDirtyCount ); + assertEquals( 2, Strategy.INSTANCE.resetDirtyCount ); assertEquals( 2, Strategy.INSTANCE.findDirtyCount ); session = openSession(); diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/dirtiness/HHH11866Test.java b/hibernate-core/src/test/java/org/hibernate/orm/test/dirtiness/HHH11866Test.java new file mode 100644 index 000000000000..4d552465fe9b --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/dirtiness/HHH11866Test.java @@ -0,0 +1,169 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.dirtiness; + +import jakarta.persistence.Access; +import jakarta.persistence.AccessType; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.Transient; +import org.hibernate.CustomEntityDirtinessStrategy; +import org.hibernate.Session; +import org.hibernate.cfg.AvailableSettings; +import org.hibernate.persister.entity.EntityPersister; +import org.hibernate.query.MutationQuery; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.JiraKey; +import org.hibernate.testing.orm.junit.ServiceRegistry; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.hibernate.testing.orm.junit.Setting; +import org.junit.jupiter.api.Test; + +import java.beans.BeanInfo; +import java.beans.IntrospectionException; +import java.beans.Introspector; +import java.beans.PropertyDescriptor; +import java.lang.reflect.Method; +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +@JiraKey(value = "HHH-11866") +@DomainModel( + annotatedClasses = {HHH11866Test.Document.class}) +@ServiceRegistry( + settings = { + @Setting(name = AvailableSettings.GENERATE_STATISTICS, value = "true"), + @Setting(name = AvailableSettings.CUSTOM_ENTITY_DIRTINESS_STRATEGY, + value = "org.hibernate.orm.test.dirtiness.HHH11866Test$EntityDirtinessStrategy") + } +) +@SessionFactory +public class HHH11866Test { + + @Test + void hhh11866Test(SessionFactoryScope scope) { + + // prepare document + scope.inTransaction( session -> { + + MutationQuery nativeMutationQuery = session.createNativeMutationQuery( + "insert into Document (id,name) values (1,'title')" ); + nativeMutationQuery.executeUpdate(); + + } ); + + // assert document + scope.inTransaction( session -> { + + final Document document = session.createQuery( "select d from Document d", Document.class ) + .getSingleResult(); + assertNotNull( document ); + assertEquals( "title", document.getName() ); + + // check that flush doesn't trigger an update + assertEquals( 0, scope.getSessionFactory().getStatistics().getEntityUpdateCount() ); + session.flush(); + assertEquals( 0, scope.getSessionFactory().getStatistics().getEntityUpdateCount() ); + } ); + } + + @Entity(name = "Document") + public static class Document extends SelfDirtyCheckingEntity { + + @Id + @GeneratedValue + Long id; + + // we need AccessType.PROPERTY to ensure that markDirtyProperty() is called + @Access(AccessType.PROPERTY) + private String name; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + markDirtyProperty(); + } + } + + public static class EntityDirtinessStrategy implements CustomEntityDirtinessStrategy { + + @Override + public boolean canDirtyCheck(Object entity, EntityPersister persister, Session session) { + return entity instanceof SelfDirtyCheckingEntity; + } + + @Override + public boolean isDirty(Object entity, EntityPersister persister, Session session) { + return !cast( entity ).getDirtyProperties().isEmpty(); + } + + @Override + public void resetDirty(Object entity, EntityPersister persister, Session session) { + cast( entity ).clearDirtyProperties(); + } + + @Override + public void findDirty(Object entity, EntityPersister persister, Session session, DirtyCheckContext dirtyCheckContext) { + final SelfDirtyCheckingEntity dirtyAware = cast( entity ); + dirtyCheckContext.doDirtyChecking( + attributeInformation -> { + String propertyName = attributeInformation.getName(); + return dirtyAware.getDirtyProperties().contains( propertyName ); + } + ); + } + + private SelfDirtyCheckingEntity cast(Object entity) { + return (SelfDirtyCheckingEntity) entity; + } + } + + public static abstract class SelfDirtyCheckingEntity { + + private final Map setterToPropertyMap = new HashMap<>(); + + @Transient + private final Set dirtyProperties = new LinkedHashSet<>(); + + public SelfDirtyCheckingEntity() { + try { + BeanInfo beanInfo = Introspector.getBeanInfo( getClass() ); + PropertyDescriptor[] descriptors = beanInfo.getPropertyDescriptors(); + for ( PropertyDescriptor descriptor : descriptors ) { + Method setter = descriptor.getWriteMethod(); + if ( setter != null ) { + setterToPropertyMap.put( setter.getName(), descriptor.getName() ); + } + } + } + catch (IntrospectionException e) { + throw new IllegalStateException( e ); + } + } + + public Set getDirtyProperties() { + return dirtyProperties; + } + + public void clearDirtyProperties() { + dirtyProperties.clear(); + } + + protected void markDirtyProperty() { + String methodName = Thread.currentThread().getStackTrace()[2].getMethodName(); + dirtyProperties.add( setterToPropertyMap.get( methodName ) ); + } + } +}