diff --git a/hibernate-core/src/main/java/org/hibernate/annotations/CurrentTimestamp.java b/hibernate-core/src/main/java/org/hibernate/annotations/CurrentTimestamp.java index 5bcbad1d3358..18fb8a26cf9c 100644 --- a/hibernate-core/src/main/java/org/hibernate/annotations/CurrentTimestamp.java +++ b/hibernate-core/src/main/java/org/hibernate/annotations/CurrentTimestamp.java @@ -14,6 +14,7 @@ import static java.lang.annotation.ElementType.FIELD; import static java.lang.annotation.ElementType.METHOD; import static java.lang.annotation.RetentionPolicy.RUNTIME; +import static org.hibernate.generator.EventType.FORCE_INCREMENT; import static org.hibernate.generator.EventType.INSERT; import static org.hibernate.generator.EventType.UPDATE; @@ -75,7 +76,7 @@ * If it should be generated just once, on the initial SQL {@code insert}, * explicitly specify {@link EventType#INSERT event = INSERT}. */ - EventType[] event() default {INSERT, UPDATE}; + EventType[] event() default {INSERT, UPDATE, FORCE_INCREMENT}; /** * Specifies how the timestamp is generated. By default, it is generated diff --git a/hibernate-core/src/main/java/org/hibernate/engine/internal/Nullability.java b/hibernate-core/src/main/java/org/hibernate/engine/internal/Nullability.java index f30c00bf424a..5700d4a11efa 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/internal/Nullability.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/internal/Nullability.java @@ -171,7 +171,7 @@ else if ( propertyType instanceof ComponentType componentType ) { else if ( propertyType instanceof CollectionType collectionType ) { // persistent collections may have components if ( collectionType.getElementType( session.getFactory() ) instanceof CompositeType componentType ) { - // check for all components values in the collection + // check for all component's values in the collection final Iterator iterator = getLoadedElementsIterator( collectionType, value ); while ( iterator.hasNext() ) { final Object compositeElement = iterator.next(); diff --git a/hibernate-core/src/main/java/org/hibernate/generator/EventType.java b/hibernate-core/src/main/java/org/hibernate/generator/EventType.java index 6cfd46a6298c..d65dfd27062c 100644 --- a/hibernate-core/src/main/java/org/hibernate/generator/EventType.java +++ b/hibernate-core/src/main/java/org/hibernate/generator/EventType.java @@ -4,6 +4,8 @@ */ package org.hibernate.generator; +import org.hibernate.Incubating; + /** * Enumerates event types that can result in generation of a new value. * A {@link Generator} must specify which events it responds to, by @@ -35,5 +37,12 @@ public enum EventType { * This indicates, for example, that a version number should be * incremented. */ - UPDATE + UPDATE, + /** + * An event that occurs during verification of a lock of type + * of {@link org.hibernate.LockMode#OPTIMISTIC_FORCE_INCREMENT} + * or {@link org.hibernate.LockMode#PESSIMISTIC_FORCE_INCREMENT}. + */ + @Incubating + FORCE_INCREMENT } diff --git a/hibernate-core/src/main/java/org/hibernate/generator/Generator.java b/hibernate-core/src/main/java/org/hibernate/generator/Generator.java index 2e8f96c7b9c1..a379444fa85a 100644 --- a/hibernate-core/src/main/java/org/hibernate/generator/Generator.java +++ b/hibernate-core/src/main/java/org/hibernate/generator/Generator.java @@ -4,11 +4,13 @@ */ package org.hibernate.generator; +import org.hibernate.Incubating; import org.hibernate.engine.spi.SharedSessionContractImplementor; import java.io.Serializable; import java.util.EnumSet; +import static org.hibernate.generator.EventType.FORCE_INCREMENT; import static org.hibernate.generator.EventType.INSERT; import static org.hibernate.generator.EventType.UPDATE; @@ -183,7 +185,8 @@ default boolean allowMutation() { } default boolean generatesSometimes() { - return !getEventTypes().isEmpty(); + return generatesOnInsert() + || generatesOnUpdate(); } default boolean generatesOnInsert() { @@ -193,4 +196,9 @@ default boolean generatesOnInsert() { default boolean generatesOnUpdate() { return getEventTypes().contains(UPDATE); } + + @Incubating + default boolean generatesOnForceIncrement() { + return getEventTypes().contains(FORCE_INCREMENT); + } } diff --git a/hibernate-core/src/main/java/org/hibernate/generator/internal/CurrentTimestampGeneration.java b/hibernate-core/src/main/java/org/hibernate/generator/internal/CurrentTimestampGeneration.java index 4aa62c96be74..943fdc6b7882 100644 --- a/hibernate-core/src/main/java/org/hibernate/generator/internal/CurrentTimestampGeneration.java +++ b/hibernate-core/src/main/java/org/hibernate/generator/internal/CurrentTimestampGeneration.java @@ -5,6 +5,10 @@ package org.hibernate.generator.internal; import java.lang.reflect.Member; +import java.sql.CallableStatement; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; import java.sql.Time; import java.sql.Timestamp; import java.time.Clock; @@ -26,7 +30,6 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.function.BiFunction; -import org.hibernate.AssertionFailure; import org.hibernate.SessionFactory; import org.hibernate.annotations.CreationTimestamp; import org.hibernate.annotations.CurrentTimestamp; @@ -35,6 +38,8 @@ import org.hibernate.dialect.Dialect; import org.hibernate.engine.config.spi.ConfigurationService; import org.hibernate.engine.jdbc.Size; +import org.hibernate.engine.jdbc.spi.JdbcCoordinator; +import org.hibernate.engine.jdbc.spi.StatementPreparer; import org.hibernate.engine.spi.SharedSessionContractImplementor; import org.hibernate.generator.BeforeExecutionGenerator; import org.hibernate.generator.EventType; @@ -45,7 +50,10 @@ import org.hibernate.type.descriptor.java.ClockHelper; import org.checkerframework.checker.nullness.qual.Nullable; +import org.hibernate.type.descriptor.java.JavaType; +import static java.sql.Types.TIMESTAMP; +import static org.hibernate.engine.jdbc.JdbcLogging.JDBC_MESSAGE_LOGGER; import static org.hibernate.generator.EventTypeSets.INSERT_AND_UPDATE; import static org.hibernate.generator.EventTypeSets.INSERT_ONLY; import static org.hibernate.generator.EventTypeSets.fromArray; @@ -80,6 +88,8 @@ public class CurrentTimestampGeneration implements BeforeExecutionGenerator, OnE private final EnumSet eventTypes; + private final JavaType propertyType; + private final CurrentTimestampGeneratorDelegate delegate; private static final Map, BiFunction<@Nullable Clock, Integer, CurrentTimestampGeneratorDelegate>> GENERATOR_PRODUCERS = new HashMap<>(); private static final Map GENERATOR_DELEGATES = new ConcurrentHashMap<>(); @@ -182,19 +192,27 @@ public class CurrentTimestampGeneration implements BeforeExecutionGenerator, OnE ); } + private static JavaType getPropertyType(GeneratorCreationContext context) { + return context.getDatabase().getTypeConfiguration().getJavaTypeRegistry() + .getDescriptor( context.getProperty().getType().getReturnedClass() ); + } + public CurrentTimestampGeneration(CurrentTimestamp annotation, Member member, GeneratorCreationContext context) { delegate = getGeneratorDelegate( annotation.source(), member, context ); eventTypes = fromArray( annotation.event() ); + propertyType = getPropertyType( context ); } public CurrentTimestampGeneration(CreationTimestamp annotation, Member member, GeneratorCreationContext context) { delegate = getGeneratorDelegate( annotation.source(), member, context ); eventTypes = INSERT_ONLY; + propertyType = getPropertyType( context ); } public CurrentTimestampGeneration(UpdateTimestamp annotation, Member member, GeneratorCreationContext context) { delegate = getGeneratorDelegate( annotation.source(), member, context ); eventTypes = INSERT_AND_UPDATE; + propertyType = getPropertyType( context ); } private static CurrentTimestampGeneratorDelegate getGeneratorDelegate( @@ -208,36 +226,42 @@ static CurrentTimestampGeneratorDelegate getGeneratorDelegate( SourceType source, Class propertyType, GeneratorCreationContext context) { - switch (source) { - case VM: + return switch (source) { + case DB -> null; + case VM -> { // Generator is only used for in-VM generation - final BasicValue basicValue = (BasicValue) context.getProperty().getValue(); - final Size size = basicValue.getColumns().get( 0 ).getColumnSize( - context.getDatabase().getDialect(), - basicValue.getMetadata() - ); - final Clock baseClock = - context.getServiceRegistry().requireService( ConfigurationService.class ) - .getSetting( CLOCK_SETTING_NAME, value -> (Clock) value ); - final Key key = new Key( propertyType, baseClock, size.getPrecision() == null ? 0 : size.getPrecision() ); - final CurrentTimestampGeneratorDelegate delegate = GENERATOR_DELEGATES.get( key ); + final Key key = new Key( propertyType, getBaseClock( context ), getPrecision( context ) ); + final var delegate = GENERATOR_DELEGATES.get( key ); if ( delegate != null ) { - return delegate; - } - final var producer = GENERATOR_PRODUCERS.get( key.clazz ); - if ( producer == null ) { - return null; + yield delegate; } else { - final var generatorDelegate = producer.apply( key.clock, key.precision ); - final var old = GENERATOR_DELEGATES.putIfAbsent( key, generatorDelegate ); - return old != null ? old : generatorDelegate; + final var producer = GENERATOR_PRODUCERS.get( key.clazz ); + if ( producer == null ) { + yield null; + } + else { + final var generatorDelegate = producer.apply( key.clock, key.precision ); + final var old = GENERATOR_DELEGATES.putIfAbsent( key, generatorDelegate ); + yield old != null ? old : generatorDelegate; + } } - case DB: - return null; - default: - throw new AssertionFailure("unknown source"); - } + } + }; + } + + private static int getPrecision(GeneratorCreationContext context) { + final BasicValue basicValue = (BasicValue) context.getProperty().getValue(); + final Size size = + basicValue.getColumns().get( 0 ) + .getColumnSize( context.getDatabase().getDialect(), + basicValue.getMetadata() ); + return size.getPrecision() == null ? 0 : size.getPrecision(); + } + + private static Clock getBaseClock(GeneratorCreationContext context) { + return context.getServiceRegistry().requireService( ConfigurationService.class ) + .getSetting( CLOCK_SETTING_NAME, value -> (Clock) value ); } public static T getClock(SessionFactory sessionFactory) { @@ -256,7 +280,15 @@ public EnumSet getEventTypes() { @Override public Object generate(SharedSessionContractImplementor session, Object owner, Object currentValue, EventType eventType) { - return delegate.generate(); + if ( delegate == null ) { + if ( eventType != EventType.FORCE_INCREMENT ) { + throw new UnsupportedOperationException( "CurrentTimestampGeneration.generate() should not have been called" ); + } + return propertyType.wrap( getCurrentTimestamp( session ), session ); + } + else { + return delegate.generate(); + } } @Override @@ -281,4 +313,51 @@ interface CurrentTimestampGeneratorDelegate { private record Key(Class clazz, @Nullable Clock clock, int precision) { } + + static Timestamp getCurrentTimestamp(SharedSessionContractImplementor session) { + final Dialect dialect = session.getJdbcServices().getJdbcEnvironment().getDialect(); + final boolean callable = dialect.isCurrentTimestampSelectStringCallable(); + final String timestampSelectString = dialect.getCurrentTimestampSelectString(); + final JdbcCoordinator coordinator = session.getJdbcCoordinator(); + final StatementPreparer statementPreparer = coordinator.getStatementPreparer(); + PreparedStatement statement = null; + try { + statement = statementPreparer.prepareStatement( timestampSelectString, callable ); + final Timestamp ts = callable + ? extractCalledResult( statement, coordinator, timestampSelectString ) + : extractResult( statement, coordinator, timestampSelectString ); + if ( JDBC_MESSAGE_LOGGER.isTraceEnabled() ) { + JDBC_MESSAGE_LOGGER.currentTimestampRetrievedFromDatabase( ts, ts.getNanos(), ts.getTime() ); + } + return ts; + } + catch (SQLException e) { + throw session.getJdbcServices().getSqlExceptionHelper().convert( + e, + "could not obtain current timestamp from database", + timestampSelectString + ); + } + finally { + if ( statement != null ) { + coordinator.getLogicalConnection().getResourceRegistry().release( statement ); + coordinator.afterStatementExecution(); + } + } + } + + static Timestamp extractResult(PreparedStatement statement, JdbcCoordinator coordinator, String sql) + throws SQLException { + final ResultSet resultSet = coordinator.getResultSetReturn().extract( statement, sql ); + resultSet.next(); + return resultSet.getTimestamp( 1 ); + } + + static Timestamp extractCalledResult(PreparedStatement statement, JdbcCoordinator coordinator, String sql) + throws SQLException { + final CallableStatement callable = (CallableStatement) statement; + callable.registerOutParameter( 1, TIMESTAMP ); + coordinator.getResultSetReturn().execute( callable, sql ); + return callable.getTimestamp( 1 ); + } } diff --git a/hibernate-core/src/main/java/org/hibernate/generator/internal/SourceGeneration.java b/hibernate-core/src/main/java/org/hibernate/generator/internal/SourceGeneration.java index fa88c34e57de..da1a7d3d2559 100644 --- a/hibernate-core/src/main/java/org/hibernate/generator/internal/SourceGeneration.java +++ b/hibernate-core/src/main/java/org/hibernate/generator/internal/SourceGeneration.java @@ -8,8 +8,6 @@ import org.hibernate.annotations.Source; import org.hibernate.annotations.SourceType; import org.hibernate.dialect.Dialect; -import org.hibernate.engine.jdbc.spi.JdbcCoordinator; -import org.hibernate.engine.jdbc.spi.StatementPreparer; import org.hibernate.engine.spi.SharedSessionContractImplementor; import org.hibernate.generator.EventType; import org.hibernate.generator.EventTypeSets; @@ -19,16 +17,9 @@ import java.lang.reflect.Member; -import java.sql.CallableStatement; -import java.sql.PreparedStatement; -import java.sql.ResultSet; -import java.sql.SQLException; -import java.sql.Timestamp; import java.util.EnumSet; -import static java.sql.Types.TIMESTAMP; -import static org.hibernate.engine.jdbc.JdbcLogging.JDBC_MESSAGE_LOGGER; -import static org.hibernate.generator.EventTypeSets.INSERT_AND_UPDATE; +import static org.hibernate.generator.internal.CurrentTimestampGeneration.getCurrentTimestamp; /** * Value generation strategy using the query {@link Dialect#getCurrentTimestampSelectString()}. @@ -63,11 +54,11 @@ public SourceGeneration(SourceType sourceType, Class propertyType, GeneratorC } /** - * @return {@link EventTypeSets#INSERT_ONLY} + * @return {@link EventTypeSets#ALL} */ @Override public EnumSet getEventTypes() { - return INSERT_AND_UPDATE; + return EventTypeSets.ALL; } @Override @@ -76,52 +67,4 @@ public Object generate(SharedSessionContractImplementor session, Object owner, O ? propertyType.wrap( getCurrentTimestamp( session ), session ) : valueGenerator.generate(); } - - private Timestamp getCurrentTimestamp(SharedSessionContractImplementor session) { - final Dialect dialect = session.getJdbcServices().getJdbcEnvironment().getDialect(); - final boolean callable = dialect.isCurrentTimestampSelectStringCallable(); - final String timestampSelectString = dialect.getCurrentTimestampSelectString(); - final JdbcCoordinator coordinator = session.getJdbcCoordinator(); - final StatementPreparer statementPreparer = coordinator.getStatementPreparer(); - PreparedStatement statement = null; - try { - statement = statementPreparer.prepareStatement( timestampSelectString, callable ); - final Timestamp ts = callable - ? extractCalledResult( statement, coordinator, timestampSelectString ) - : extractResult( statement, coordinator, timestampSelectString ); - if ( JDBC_MESSAGE_LOGGER.isTraceEnabled() ) { - JDBC_MESSAGE_LOGGER.currentTimestampRetrievedFromDatabase( ts, ts.getNanos(), ts.getTime() ); - } - return ts; - } - catch (SQLException e) { - throw session.getJdbcServices().getSqlExceptionHelper().convert( - e, - "could not obtain current timestamp from database", - timestampSelectString - ); - } - finally { - if ( statement != null ) { - coordinator.getLogicalConnection().getResourceRegistry().release( statement ); - coordinator.afterStatementExecution(); - } - } - } - - private static Timestamp extractResult(PreparedStatement statement, JdbcCoordinator coordinator, String sql) - throws SQLException { - final ResultSet resultSet = coordinator.getResultSetReturn().extract( statement, sql ); - resultSet.next(); - return resultSet.getTimestamp( 1 ); - } - - private static Timestamp extractCalledResult(PreparedStatement statement, JdbcCoordinator coordinator, String sql) - throws SQLException { - final CallableStatement callable = (CallableStatement) statement; - callable.registerOutParameter( 1, TIMESTAMP ); - coordinator.getResultSetReturn().execute( callable, sql ); - return callable.getTimestamp( 1 ); - } - } diff --git a/hibernate-core/src/main/java/org/hibernate/generator/internal/VersionGeneration.java b/hibernate-core/src/main/java/org/hibernate/generator/internal/VersionGeneration.java index 8f33f93af70e..a0aef3e89b71 100644 --- a/hibernate-core/src/main/java/org/hibernate/generator/internal/VersionGeneration.java +++ b/hibernate-core/src/main/java/org/hibernate/generator/internal/VersionGeneration.java @@ -14,7 +14,7 @@ import static org.hibernate.engine.internal.Versioning.increment; import static org.hibernate.engine.internal.Versioning.seed; import static org.hibernate.generator.EventType.INSERT; -import static org.hibernate.generator.EventTypeSets.INSERT_AND_UPDATE; +import static org.hibernate.generator.EventTypeSets.ALL; /** * A default {@link org.hibernate.generator.Generator} for {@link jakarta.persistence.Version @Version} @@ -39,7 +39,7 @@ public VersionGeneration(EntityVersionMapping versionMapping) { @Override public EnumSet getEventTypes() { - return INSERT_AND_UPDATE; + return ALL; } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/persister/entity/AbstractEntityPersister.java b/hibernate-core/src/main/java/org/hibernate/persister/entity/AbstractEntityPersister.java index 58f1959ccc4e..fc403c85b76c 100644 --- a/hibernate-core/src/main/java/org/hibernate/persister/entity/AbstractEntityPersister.java +++ b/hibernate-core/src/main/java/org/hibernate/persister/entity/AbstractEntityPersister.java @@ -283,6 +283,7 @@ import static org.hibernate.engine.internal.ManagedTypeHelper.asPersistentAttributeInterceptable; import static org.hibernate.engine.internal.ManagedTypeHelper.isPersistentAttributeInterceptable; import static org.hibernate.engine.internal.ManagedTypeHelper.processIfPersistentAttributeInterceptable; +import static org.hibernate.generator.EventType.FORCE_INCREMENT; import static org.hibernate.generator.EventType.INSERT; import static org.hibernate.generator.EventType.UPDATE; import static org.hibernate.internal.util.ReflectHelper.isAbstractClass; @@ -2062,7 +2063,8 @@ public Object forceVersionIncrement( Object id, Object currentVersion, boolean batching, - SharedSessionContractImplementor session) throws HibernateException { + SharedSessionContractImplementor session) + throws HibernateException { assert getMappedTableDetails().getTableName().equals( getVersionedTableName() ); final Object nextVersion = calculateNextVersion( id, currentVersion, session ); updateCoordinator.forceVersionIncrement( id, currentVersion, nextVersion, batching, session ); @@ -2070,37 +2072,49 @@ public Object forceVersionIncrement( } private Object calculateNextVersion(Object id, Object currentVersion, SharedSessionContractImplementor session) { - if ( !isVersioned() ) { - throw new AssertionFailure( "cannot force version increment on non-versioned entity" ); - } - - if ( isVersionGeneratedOnExecution() ) { - // the difficulty here is exactly what we update in order to - // force the version to be incremented in the db... - throw new HibernateException( "LockMode.FORCE is currently not supported for generated version properties" ); - - } - - final EntityVersionMapping versionMapping = getVersionMapping(); - final Object nextVersion = getVersionJavaType().next( - currentVersion, - versionMapping.getLength(), - versionMapping.getTemporalPrecision() != null - ? versionMapping.getTemporalPrecision() - : versionMapping.getPrecision(), - versionMapping.getScale(), - session - ); + assert isVersioned(); + final Object nextVersion = + generatorForForceIncrement() + // TODO: pass in owner entity + .generate( session, null, currentVersion, FORCE_INCREMENT ); if ( LOG.isTraceEnabled() ) { + final var versionType = getVersionType(); LOG.trace( - "Forcing version increment [" + infoString( this, id, getFactory() ) + "; " - + getVersionType().toLoggableString( currentVersion, getFactory() ) + " -> " - + getVersionType().toLoggableString( nextVersion, getFactory() ) + "]" + "Forcing version increment [" + infoString( this, id, factory ) + "; " + + versionType.toLoggableString( currentVersion, factory ) + " -> " + + versionType.toLoggableString( nextVersion, factory ) + "]" ); } return nextVersion; } + private BeforeExecutionGenerator generatorForForceIncrement() { + if ( versionPropertyGenerator() instanceof BeforeExecutionGenerator generator + && generator.generatesOnForceIncrement() ) { + // Special case to accommodate the fact that we don't yet + // allow OnExecutionGenerators with force-increment locking. + // When possible, falls back to treating the generator as a + // BeforeExecutionGenerator. In particular, this works for + // CurrentTimestampGeneration. But it requires an additional + // request to the database to generate the timestamp. This + // solution is neither particularly elegant nor efficient. + return generator; + } + else if ( isVersionGeneratedOnExecution() ) { + // TODO: Ideally, we would allow this case, producing an + // UPDATE statement which sets the version column. + // Then we could remove the previous special case. + throw new HibernateException( "Force-increment lock not supported for '@Version' property with OnExecutionGenerator" ); + } + else { + final var generator = getVersionGenerator(); + if ( !generator.generatesOnForceIncrement() ) { + throw new HibernateException( "Force-increment lock not supported for '@Version' generator" ); + } + return generator; + } + } + /** * Retrieve the version number */ @@ -2154,11 +2168,10 @@ protected LockingStrategy generateLocker(LockMode lockMode, Locking.Scope lockSc } private LockingStrategy getLocker(LockMode lockMode, Locking.Scope lockScope) { - if ( lockScope != Locking.Scope.ROOT_ONLY ) { - // be sure to not use the cached form if any form of extended locking is requested - return generateLocker( lockMode, lockScope ); - } - return lockers.computeIfAbsent( lockMode, (l) -> generateLocker( lockMode, lockScope ) ); + return lockScope != Locking.Scope.ROOT_ONLY + // be sure to not use the cached form if any form of extended locking is requested + ? generateLocker( lockMode, lockScope ) + : lockers.computeIfAbsent( lockMode, (l) -> generateLocker( lockMode, lockScope ) ); } @Override @@ -2167,8 +2180,10 @@ public void lock( Object version, Object object, LockMode lockMode, - SharedSessionContractImplementor session) throws HibernateException { - getLocker( lockMode, Locking.Scope.ROOT_ONLY ).lock( id, version, object, Timeouts.WAIT_FOREVER, session ); + SharedSessionContractImplementor session) + throws HibernateException { + getLocker( lockMode, Locking.Scope.ROOT_ONLY ) + .lock( id, version, object, Timeouts.WAIT_FOREVER, session ); } @Override @@ -2182,8 +2197,10 @@ public void lock( Object version, Object object, LockOptions lockOptions, - SharedSessionContractImplementor session) throws HibernateException { - getLocker( lockOptions.getLockMode(), lockOptions.getScope() ).lock( id, version, object, lockOptions.getTimeout(), session ); + SharedSessionContractImplementor session) + throws HibernateException { + getLocker( lockOptions.getLockMode(), lockOptions.getScope() ) + .lock( id, version, object, lockOptions.getTimeout(), session ); } @Override @@ -3816,9 +3833,8 @@ public boolean hasIdentifierProperty() { @Override public BasicType getVersionType() { - return entityMetamodel.getVersionProperty() == null - ? null - : (BasicType) entityMetamodel.getVersionProperty().getType(); + final var versionProperty = entityMetamodel.getVersionProperty(); + return versionProperty == null ? null : (BasicType) versionProperty.getType(); } @Override diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/locking/CurrentTimestampVersionTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/locking/CurrentTimestampVersionTest.java new file mode 100644 index 000000000000..4602279ceeae --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/locking/CurrentTimestampVersionTest.java @@ -0,0 +1,50 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.locking; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.LockModeType; +import jakarta.persistence.Version; +import org.hibernate.annotations.CurrentTimestamp; +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 static org.junit.jupiter.api.Assertions.assertNotNull; + +@Jpa(annotatedClasses = CurrentTimestampVersionTest.Timestamped.class) +class CurrentTimestampVersionTest { + @Test void test(EntityManagerFactoryScope scope) { + Timestamped t = scope.fromTransaction( entityManager -> { + Timestamped timestamped = new Timestamped(); + timestamped.id = 1L; + entityManager.persist( timestamped ); + return timestamped; + } ); + assertNotNull( t.timestamp ); + scope.inTransaction( entityManager -> { + Timestamped timestamped = entityManager.find( Timestamped.class, 1L, + LockModeType.OPTIMISTIC_FORCE_INCREMENT ); + assertNotNull( timestamped ); + assertNotNull( timestamped.timestamp ); + } ); + scope.inTransaction( entityManager -> { + Timestamped timestamped = entityManager.find( Timestamped.class, 1L ); + timestamped.content = "new content"; + } ); + // TODO: assert some stuff about the timestamp values + } + @Entity + static class Timestamped { + @Id + Long id; + @CurrentTimestamp @Version + LocalDateTime timestamp; + String content; + } +}