diff --git a/hibernate-core/src/main/java/org/hibernate/tuple/entity/EntityMetamodel.java b/hibernate-core/src/main/java/org/hibernate/tuple/entity/EntityMetamodel.java index dc41eca1d28f..10bb21d96862 100644 --- a/hibernate-core/src/main/java/org/hibernate/tuple/entity/EntityMetamodel.java +++ b/hibernate-core/src/main/java/org/hibernate/tuple/entity/EntityMetamodel.java @@ -310,8 +310,9 @@ public EntityMetamodel( propertyInsertability[i] = writePropertyValue( (OnExecutionGenerator) generator ); } foundPostInsertGeneratedValues = foundPostInsertGeneratedValues - || generator instanceof OnExecutionGenerator; + || generatedOnExecution; foundPreInsertGeneratedValues = foundPreInsertGeneratedValues + || !generatedOnExecution || generator instanceof BeforeExecutionGenerator; } else if ( !allowMutation ) { @@ -321,9 +322,10 @@ else if ( !allowMutation ) { if ( generatedOnExecution ) { propertyUpdateability[i] = writePropertyValue( (OnExecutionGenerator) generator ); } - foundPostUpdateGeneratedValues = foundPostUpdateGeneratedValues - || generator instanceof OnExecutionGenerator; - foundPreUpdateGeneratedValues = foundPreUpdateGeneratedValues + foundPostUpdateGeneratedValues = foundPostInsertGeneratedValues + || generatedOnExecution; + foundPreUpdateGeneratedValues = foundPreInsertGeneratedValues + || !generatedOnExecution || generator instanceof BeforeExecutionGenerator; } else if ( !allowMutation ) { diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/InMemoryTimestampGenerationBatchTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/InMemoryTimestampGenerationBatchTest.java new file mode 100644 index 000000000000..b79d0be58652 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/InMemoryTimestampGenerationBatchTest.java @@ -0,0 +1,143 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.annotations; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import org.hibernate.annotations.CreationTimestamp; +import org.hibernate.annotations.SourceType; +import org.hibernate.annotations.UpdateTimestamp; +import org.hibernate.cfg.AvailableSettings; +import org.hibernate.generator.internal.CurrentTimestampGeneration; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.Jira; +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.hibernate.testing.orm.junit.SettingProvider; +import org.junit.jupiter.api.Test; + +import java.time.Instant; +import java.util.stream.IntStream; + +import static java.lang.Thread.sleep; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@DomainModel(annotatedClasses = InMemoryTimestampGenerationBatchTest.Person.class) +@SessionFactory(generateStatistics = true) +@ServiceRegistry(settings = @Setting(name = AvailableSettings.STATEMENT_BATCH_SIZE, value = "5"), + settingProviders = @SettingProvider(settingName = CurrentTimestampGeneration.CLOCK_SETTING_NAME, + provider = InMemoryTimestampGenerationBatchTest.MutableClockProvider.class)) +@Jira("https://hibernate.atlassian.net/browse/HHH-19840") +public class InMemoryTimestampGenerationBatchTest { + private static final MutableClock clock = new MutableClock(); + + private static final int PERSON_COUNT = 8; + + @Test + public void test(SessionFactoryScope scope) throws InterruptedException { + final var statistics = scope.getSessionFactory().getStatistics(); + scope.inTransaction( session -> { + Person person = null; + for ( int i = 1; i <= PERSON_COUNT; i++ ) { + person = new Person(); + person.setId( (long) i ); + person.setName( "person_" + i ); + session.persist( person ); + } + + statistics.clear(); + session.flush(); + + assertEquals( 1, statistics.getPrepareStatementCount(), "Expected updates to execute in batches" ); + + assertNotNull( person.getCreatedOn() ); + assertNotNull( person.getUpdatedOn() ); + } ); + + + clock.tick(); + sleep( 1 ); + + scope.inTransaction( session -> { + final var persons = session.findMultiple( Person.class, + IntStream.rangeClosed( 1, PERSON_COUNT ) + .mapToObj( i -> (long) i ) + .toList() ); + + assertThat( persons ).hasSize( PERSON_COUNT ); + assertThat( persons ).doesNotContainNull(); + + Person person = null; + for ( final Person p : persons ) { + p.setName( p.getName() + "_updated" ); + person = p; + } + + final var createdOn = person.getCreatedOn(); + final var updatedOn = person.getUpdatedOn(); + + statistics.clear(); + session.flush(); + + assertEquals( 1, statistics.getPrepareStatementCount(), "Expected updates to execute in batches" ); + + assertEquals( person.getCreatedOn(), createdOn ); + assertTrue( person.getUpdatedOn().isAfter( updatedOn ) ); + } ); + } + + public static class MutableClockProvider implements SettingProvider.Provider { + @Override + public Object getSetting() { + return clock; + } + } + + @Entity(name = "Person") + public static class Person { + @Id + private Long id; + + private String name; + + @Column(nullable = false) + @CreationTimestamp(source = SourceType.VM) + private Instant createdOn; + + @Column(nullable = false) + @UpdateTimestamp(source = SourceType.VM) + private Instant updatedOn; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Instant getCreatedOn() { + return createdOn; + } + + public Instant getUpdatedOn() { + return updatedOn; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/InMemoryTimestampGenerationTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/InMemoryTimestampGenerationTest.java new file mode 100644 index 000000000000..9854a794646b --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/InMemoryTimestampGenerationTest.java @@ -0,0 +1,122 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.annotations; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import org.hibernate.annotations.CreationTimestamp; +import org.hibernate.annotations.SourceType; +import org.hibernate.annotations.UpdateTimestamp; +import org.hibernate.generator.internal.CurrentTimestampGeneration; +import org.hibernate.testing.orm.junit.EntityManagerFactoryScope; +import org.hibernate.testing.orm.junit.Jira; +import org.hibernate.testing.orm.junit.Jpa; +import org.hibernate.testing.orm.junit.SettingProvider; +import org.junit.jupiter.api.Test; + +import java.time.Instant; + +import static java.lang.Thread.sleep; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@Jpa(annotatedClasses = InMemoryTimestampGenerationTest.Person.class, + settingProviders = @SettingProvider(settingName = CurrentTimestampGeneration.CLOCK_SETTING_NAME, + provider = InMemoryTimestampGenerationTest.MutableClockProvider.class)) +@Jira("https://hibernate.atlassian.net/browse/HHH-19840") +public class InMemoryTimestampGenerationTest { + private static final MutableClock clock = new MutableClock(); + + @Test + public void test(EntityManagerFactoryScope scope) throws InterruptedException { + scope.inTransaction( entityManager -> { + Person person = new Person(); + person.setId( 1L ); + person.setFirstName( "Jon" ); + person.setLastName( "Doe" ); + entityManager.persist( person ); + + entityManager.flush(); + + assertNotNull( person.getCreatedOn() ); + assertNotNull( person.getUpdatedOn() ); + } ); + + clock.tick(); + sleep( 1 ); + + scope.inTransaction( entityManager -> { + final Person person = entityManager.find( Person.class, 1L ); + person.setLastName( "Doe Jr." ); + + final var updatedOn = person.getUpdatedOn(); + final var createdOn = person.getCreatedOn(); + + entityManager.flush(); + + assertEquals( person.getCreatedOn(), createdOn ); + assertTrue( person.getUpdatedOn().isAfter( updatedOn ) ); + } ); + } + + static class MutableClockProvider implements SettingProvider.Provider { + @Override + public Object getSetting() { + return clock; + } + } + + @Entity(name = "Person") + static class Person { + @Id + private Long id; + + private String firstName; + + private String lastName; + + @Column(nullable = false) + @CreationTimestamp(source= SourceType.VM) + private Instant createdOn; + + @Column(nullable = false) + @UpdateTimestamp(source= SourceType.VM) + private Instant updatedOn; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getFirstName() { + return firstName; + } + + public void setFirstName(String firstName) { + this.firstName = firstName; + } + + public String getLastName() { + return lastName; + } + + public void setLastName(String lastName) { + this.lastName = lastName; + } + + public Instant getCreatedOn() { + return createdOn; + } + + public Instant getUpdatedOn() { + return updatedOn; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/InMemoryUpdateTimestampTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/InMemoryUpdateTimestampTest.java deleted file mode 100644 index 62c81dd0cc9b..000000000000 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/InMemoryUpdateTimestampTest.java +++ /dev/null @@ -1,117 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * Copyright Red Hat Inc. and Hibernate Authors - */ -package org.hibernate.orm.test.annotations; - -import java.util.Date; -import java.util.Map; -import java.util.concurrent.atomic.AtomicReference; -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.Id; - -import org.hibernate.annotations.UpdateTimestamp; -import org.hibernate.generator.internal.CurrentTimestampGeneration; -import org.hibernate.orm.test.jpa.BaseEntityManagerFunctionalTestCase; - -import org.hibernate.testing.orm.junit.JiraKey; -import org.junit.Assert; -import org.junit.Test; - -import static org.hibernate.testing.transaction.TransactionUtil.doInJPA; -import static org.junit.Assert.assertTrue; - -/** - * @author Vlad Mihalcea - */ -@JiraKey("HHH-13256") -public class InMemoryUpdateTimestampTest extends BaseEntityManagerFunctionalTestCase { - - private static final MutableClock clock = new MutableClock(); - - @Override - protected Class[] getAnnotatedClasses() { - return new Class[] { - Person.class - }; - } - - @Override - protected void addConfigOptions(Map options) { - super.addConfigOptions( options ); - options.put( CurrentTimestampGeneration.CLOCK_SETTING_NAME, clock ); - } - - @Test - public void test() { - doInJPA( this::entityManagerFactory, entityManager -> { - Person person = new Person(); - person.setId( 1L ); - person.setFirstName( "Jon" ); - person.setLastName( "Doe" ); - entityManager.persist( person ); - - entityManager.flush(); - Assert.assertNotNull( person.getUpdatedOn() ); - } ); - clock.tick(); - - AtomicReference beforeTimestamp = new AtomicReference<>(); - - sleep( 1 ); - - Person _person = doInJPA( this::entityManagerFactory, entityManager -> { - Person person = entityManager.find( Person.class, 1L ); - beforeTimestamp.set( person.getUpdatedOn() ); - person.setLastName( "Doe Jr." ); - - return person; - } ); - - assertTrue( _person.getUpdatedOn().after( beforeTimestamp.get() ) ); - } - - @Entity(name = "Person") - public static class Person { - - @Id - private Long id; - - private String firstName; - - private String lastName; - - @Column(nullable = false) - @UpdateTimestamp - private Date updatedOn; - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getFirstName() { - return firstName; - } - - public void setFirstName(String firstName) { - this.firstName = firstName; - } - - public String getLastName() { - return lastName; - } - - public void setLastName(String lastName) { - this.lastName = lastName; - } - - public Date getUpdatedOn() { - return updatedOn; - } - } -}