diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/event/impl/DefaultReactiveFlushEntityEventListener.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/event/impl/DefaultReactiveFlushEntityEventListener.java index b8e6159d1..5e77e79bd 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/event/impl/DefaultReactiveFlushEntityEventListener.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/event/impl/DefaultReactiveFlushEntityEventListener.java @@ -13,6 +13,7 @@ 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; @@ -42,10 +43,12 @@ import java.util.Arrays; 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.processIfManagedEntity; import static org.hibernate.engine.internal.ManagedTypeHelper.processIfSelfDirtinessTracker; import static org.hibernate.engine.internal.Versioning.getVersion; import static org.hibernate.engine.internal.Versioning.incrementVersion; @@ -223,6 +226,8 @@ private boolean isUpdateNecessary(final FlushEntityEvent event, final boolean mi else { final Object entity = event.getEntity(); processIfSelfDirtinessTracker( entity, SelfDirtinessTracker::$$_hibernate_clearDirtyAttributes ); + processIfManagedEntity( entity, DefaultReactiveFlushEntityEventListener::useTracker ); + final EventSource source = event.getSession(); source.getFactory() .getCustomEntityDirtinessStrategy() @@ -235,6 +240,10 @@ private boolean isUpdateNecessary(final FlushEntityEvent event, final boolean mi } } + 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(); @@ -555,7 +564,7 @@ private static int[] getDirtyProperties(FlushEntityEvent event) { } else { final Object entity = event.getEntity(); - return isSelfDirtinessTracker( entity ) + return isSelfDirtinessTracker( entity ) && asManagedEntity( entity ).$$_hibernate_useTracker() ? getDirtyPropertiesFromSelfDirtinessTracker( asSelfDirtinessTracker( entity ), event ) : getDirtyPropertiesFromCustomEntityDirtinessStrategy( event ); } diff --git a/integration-tests/bytecode-enhancements-it/src/main/java/org/hibernate/reactive/it/dirtychecking/Fruit.java b/integration-tests/bytecode-enhancements-it/src/main/java/org/hibernate/reactive/it/dirtychecking/Fruit.java new file mode 100644 index 000000000..1c700e309 --- /dev/null +++ b/integration-tests/bytecode-enhancements-it/src/main/java/org/hibernate/reactive/it/dirtychecking/Fruit.java @@ -0,0 +1,35 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.reactive.it.dirtychecking; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; + +@Entity +public class Fruit { + @Id + private int id; + + // Dirty checking should not be confused by this initialization. + private String name = "Banana"; + + public int getId() { + return id; + } + + public Fruit setId(final int id) { + this.id = id; + return this; + } + + public String getName() { + return name; + } + + public Fruit setName(final String name) { + this.name = name; + return this; + } +} diff --git a/integration-tests/bytecode-enhancements-it/src/test/java/org/hibernate/reactive/it/DirtyCheckingIT.java b/integration-tests/bytecode-enhancements-it/src/test/java/org/hibernate/reactive/it/DirtyCheckingIT.java new file mode 100644 index 000000000..2533b5213 --- /dev/null +++ b/integration-tests/bytecode-enhancements-it/src/test/java/org/hibernate/reactive/it/DirtyCheckingIT.java @@ -0,0 +1,79 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.reactive.it; + +import java.util.Collection; +import java.util.List; +import java.util.concurrent.CompletionStage; + +import org.hibernate.boot.registry.StandardServiceRegistryBuilder; +import org.hibernate.cfg.Configuration; +import org.hibernate.reactive.it.dirtychecking.Fruit; + +import org.hibernate.testing.SqlStatementTracker; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import io.vertx.junit5.VertxTestContext; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.hibernate.reactive.util.impl.CompletionStages.voidFuture; + +public class DirtyCheckingIT extends BaseReactiveIT { + + private static SqlStatementTracker sqlTracker; + + @Override + protected Configuration constructConfiguration() { + Configuration configuration = super.constructConfiguration(); + + // Construct a tracker that collects query statements via the SqlStatementLogger framework. + // Pass in configuration properties to hand off any actual logging properties + sqlTracker = new SqlStatementTracker( DirtyCheckingIT::updateQueryFilter, configuration.getProperties() ); + return configuration; + } + + private static boolean updateQueryFilter(String s) { + return s.toLowerCase().startsWith( "update " ); + } + + @BeforeEach + public void clearTracker() { + sqlTracker.clear(); + } + + @Override + protected void addServices(StandardServiceRegistryBuilder builder) { + sqlTracker.registerService( builder ); + } + + @Override + protected Collection> annotatedEntities() { + return List.of( Fruit.class ); + } + + @Override + protected CompletionStage cleanDb() { + // There's only one test, so we don't need to clean the db. + // This prevents extra queries in the log when the test is over. + return voidFuture(); + } + + @Test + public void testDirtyCheck(VertxTestContext context) { + test( + context, + getMutinySessionFactory() + .withTransaction( s -> s.persist( new Fruit().setId( 5 ).setName( "Apple" ) ) ) + .chain( () -> getMutinySessionFactory().withTransaction( s -> s.find( Fruit.class, 5 ) ) ) + .invoke( fruit -> { + assertThat( fruit ).hasFieldOrPropertyWithValue( "name", "Apple" ); + assertThat( sqlTracker.getLoggedQueries() ) + .as( "Dirty field detection failed, unexpected SQL mutation query" ) + .isEmpty(); + } ) + ); + } +}