diff --git a/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/UpdateCoordinatorStandard.java b/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/UpdateCoordinatorStandard.java index 902def1887bd..367d53229aed 100644 --- a/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/UpdateCoordinatorStandard.java +++ b/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/UpdateCoordinatorStandard.java @@ -169,15 +169,16 @@ public GeneratedValues update( SharedSessionContractImplementor session) { final EntityVersionMapping versionMapping = entityPersister().getVersionMapping(); if ( versionMapping != null ) { - final Supplier generatedValuesAccess = handlePotentialImplicitForcedVersionIncrement( - entity, - id, - values, - oldVersion, - incomingDirtyAttributeIndexes, - session, - versionMapping - ); + final var generatedValuesAccess = + handlePotentialImplicitForcedVersionIncrement( + entity, + id, + values, + oldVersion, + incomingDirtyAttributeIndexes, + session, + versionMapping + ); if ( generatedValuesAccess != null ) { return generatedValuesAccess.get(); } @@ -389,57 +390,71 @@ protected Supplier handlePotentialImplicitForcedVersionIncremen int[] incomingDirtyAttributeIndexes, SharedSessionContractImplementor session, EntityVersionMapping versionMapping) { - // handle case where the only value being updated is the version. - // we handle this case specially from `#coordinateUpdate` to leverage - // `#doVersionUpdate` - final boolean isSimpleVersionUpdate; + // Handle a case where the only value being updated is the version. + // We treat this case specially in `#coordinateUpdate` to leverage + // `#doVersionUpdate`. final Object newVersion; - - if ( incomingDirtyAttributeIndexes != null ) { - if ( incomingDirtyAttributeIndexes.length == 1 - && versionMapping.getVersionAttribute() == entityPersister().getAttributeMapping( incomingDirtyAttributeIndexes[0] ) ) { - // special case of only the version attribute itself as dirty - isSimpleVersionUpdate = true; - newVersion = values[ incomingDirtyAttributeIndexes[0]]; - } - else if ( incomingDirtyAttributeIndexes.length == 0 && oldVersion != null ) { - isSimpleVersionUpdate = !versionMapping.areEqual( - values[ versionMapping.getVersionAttribute().getStateArrayPosition() ], - oldVersion, - session - ); - newVersion = values[ versionMapping.getVersionAttribute().getStateArrayPosition()]; - } - else { - isSimpleVersionUpdate = false; - newVersion = null; - } - } - else { - isSimpleVersionUpdate = false; - newVersion = null; + if ( hasUpdateGeneratedValues() ) { + // if we have any fields generated by the UPDATE event, + // then we have to include the generated fields in the + // update statement + return null; } - - if ( isSimpleVersionUpdate ) { - // we have just the version being updated - use the special handling - assert newVersion != null; - final GeneratedValues generatedValues = doVersionUpdate( entity, id, newVersion, oldVersion, session ); - return () -> generatedValues; + else if ( incomingDirtyAttributeIndexes != null ) { + switch ( incomingDirtyAttributeIndexes.length ) { + case 1: + final int dirtyAttributeIndex = incomingDirtyAttributeIndexes[0]; + final var versionAttribute = versionMapping.getVersionAttribute(); + final var dirtyAttribute = entityPersister().getAttributeMapping( dirtyAttributeIndex ); + if ( versionAttribute == dirtyAttribute ) { + // only the version attribute itself is dirty + newVersion = values[dirtyAttributeIndex]; + } + else { + // the dirty field is some other field + return null; + } + break; + case 0: + if ( oldVersion != null ) { + newVersion = values[versionMapping.getVersionAttribute().getStateArrayPosition()]; + if ( versionMapping.areEqual( newVersion, oldVersion, session ) ) { + return null; + } + } + else { + return null; + } + break; + default: + return null; + } } else { return null; } + + // we have just the version being updated - use the special handling + assert newVersion != null; + final GeneratedValues generatedValues = doVersionUpdate( entity, id, newVersion, oldVersion, session ); + return () -> generatedValues; + } + + private boolean hasUpdateGeneratedValues() { + final var entityMetamodel = entityPersister().getEntityMetamodel(); + return entityMetamodel.hasUpdateGeneratedValues() + || entityMetamodel.hasPreUpdateGeneratedValues(); } private static boolean isValueGenerated(Generator generator) { return generator != null - && generator.generatesOnUpdate() - && generator.generatedOnExecution(); + && generator.generatesOnUpdate() + && generator.generatedOnExecution(); } private static boolean isValueGenerationInSql(Generator generator, Dialect dialect) { assert isValueGenerated( generator ); - return ( (OnExecutionGenerator) generator ).referenceColumnsInSql(dialect); + return ( (OnExecutionGenerator) generator ).referenceColumnsInSql( dialect ); } /** diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/generated/GeneratedByDbOnForcedIncrementTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/generated/GeneratedByDbOnForcedIncrementTest.java new file mode 100644 index 000000000000..b193313ce8e4 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/generated/GeneratedByDbOnForcedIncrementTest.java @@ -0,0 +1,47 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.mapping.generated; + +import jakarta.persistence.ElementCollection; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Version; +import org.hibernate.annotations.UpdateTimestamp; +import org.hibernate.testing.orm.junit.EntityManagerFactoryScope; +import org.hibernate.testing.orm.junit.Jpa; +import org.junit.jupiter.api.Test; + +import java.time.LocalDateTime; +import java.util.Set; + +import static org.hibernate.annotations.SourceType.DB; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@Jpa(annotatedClasses = GeneratedByDbOnForcedIncrementTest.WithUpdateTimestamp.class) +class GeneratedByDbOnForcedIncrementTest { + @Test void test(EntityManagerFactoryScope scope) throws InterruptedException { + var persisted = scope.fromTransaction( em -> { + var entity = new WithUpdateTimestamp(); + em.persist( entity ); + return entity; + } ); + Thread.sleep( 100 ); + var updated = scope.fromTransaction( em -> { + var entity = em.find( WithUpdateTimestamp.class, 0L ); + entity.names.add( "Gavin" ); + return entity; + } ); + assertTrue( persisted.updated.isBefore( updated.updated ) ); + } + @Entity + static class WithUpdateTimestamp { + @Id long id; + @Version long version; + @UpdateTimestamp(source = DB) + LocalDateTime updated; + @ElementCollection + Set names; + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/generated/GeneratedOnForcedIncrementTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/generated/GeneratedOnForcedIncrementTest.java new file mode 100644 index 000000000000..62b416b34725 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/generated/GeneratedOnForcedIncrementTest.java @@ -0,0 +1,46 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.mapping.generated; + +import jakarta.persistence.ElementCollection; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Version; +import org.hibernate.annotations.UpdateTimestamp; +import org.hibernate.testing.orm.junit.EntityManagerFactoryScope; +import org.hibernate.testing.orm.junit.Jpa; +import org.junit.jupiter.api.Test; + +import java.time.LocalDateTime; +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +@Jpa(annotatedClasses = GeneratedOnForcedIncrementTest.WithUpdateTimestamp.class) +class GeneratedOnForcedIncrementTest { + @Test void test(EntityManagerFactoryScope scope) throws InterruptedException { + var persisted = scope.fromTransaction( em -> { + var entity = new WithUpdateTimestamp(); + em.persist( entity ); + return entity; + } ); + Thread.sleep( 100 ); + var updated = scope.fromTransaction( em -> { + var entity = em.find( WithUpdateTimestamp.class, 0L ); + entity.names.add( "Gavin" ); + return entity; + } ); + assertTrue( persisted.updated.isBefore( updated.updated ) ); + } + @Entity + static class WithUpdateTimestamp { + @Id long id; + @Version long version; + @UpdateTimestamp + LocalDateTime updated; + @ElementCollection + Set names; + } +}