Respect precision for VM generated temporal values

This commit is contained in:
Christian Beikov 2023-03-22 10:07:23 +01:00
parent dfa403b8f4
commit f68ea21891
8 changed files with 208 additions and 90 deletions

View File

@ -1016,7 +1016,7 @@ public class ModelBinder {
}
if ( versionAttributeSource.getSource().equals("db") ) {
property.setValueGeneratorCreator(
context -> new SourceGeneration( SourceType.DB, property.getType().getReturnedClass() ) );
context -> new SourceGeneration( SourceType.DB, property.getType().getReturnedClass(), context ) );
}
rootEntityDescriptor.setVersion( property );

View File

@ -12,17 +12,21 @@ import org.hibernate.annotations.CurrentTimestamp;
import org.hibernate.annotations.SourceType;
import org.hibernate.annotations.UpdateTimestamp;
import org.hibernate.dialect.Dialect;
import org.hibernate.engine.jdbc.Size;
import org.hibernate.engine.spi.SharedSessionContractImplementor;
import org.hibernate.generator.EventType;
import org.hibernate.generator.OnExecutionGenerator;
import org.hibernate.generator.BeforeExecutionGenerator;
import org.hibernate.internal.util.ReflectHelper;
import org.hibernate.generator.GeneratorCreationContext;
import org.hibernate.mapping.BasicValue;
import org.hibernate.tuple.GenerationTiming;
import org.hibernate.type.descriptor.java.ClockHelper;
import java.lang.reflect.Member;
import java.sql.Time;
import java.sql.Timestamp;
import java.time.Clock;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
@ -32,12 +36,15 @@ import java.time.OffsetDateTime;
import java.time.OffsetTime;
import java.time.Year;
import java.time.YearMonth;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.Calendar;
import java.util.Date;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.IntFunction;
import static org.hibernate.generator.EventTypeSets.INSERT_AND_UPDATE;
import static org.hibernate.generator.EventTypeSets.INSERT_ONLY;
@ -64,98 +71,158 @@ public class CurrentTimestampGeneration implements BeforeExecutionGenerator, OnE
private final EnumSet<EventType> eventTypes;
private final CurrentTimestampGeneratorDelegate delegate;
private static final Map<Class<?>, CurrentTimestampGeneratorDelegate> generatorDelegates = new HashMap<>();
private static final Map<Class<?>, IntFunction<CurrentTimestampGeneratorDelegate>> GENERATOR_PRODUCERS = new HashMap<>();
private static final Map<Key, CurrentTimestampGeneratorDelegate> GENERATOR_DELEGATES = new ConcurrentHashMap<>();
static {
generatorDelegates.put(
GENERATOR_PRODUCERS.put(
Date.class,
Date::new
);
generatorDelegates.put(
Calendar.class,
() -> {
Calendar calendar = Calendar.getInstance();
calendar.setTime( new Date() );
return calendar;
precision -> {
final Clock clock = ClockHelper.forPrecision( precision, 3 );
return () -> new Date( clock.millis() );
}
);
generatorDelegates.put(
GENERATOR_PRODUCERS.put(
Calendar.class,
precision -> {
final Clock clock = ClockHelper.forPrecision( precision, 3 );
return () -> {
Calendar calendar = Calendar.getInstance();
calendar.setTimeInMillis( clock.millis() );
return calendar;
};
}
);
GENERATOR_PRODUCERS.put(
java.sql.Date.class,
() -> new java.sql.Date( System.currentTimeMillis() )
precision -> () -> new java.sql.Date( System.currentTimeMillis() )
);
generatorDelegates.put(
GENERATOR_PRODUCERS.put(
Time.class,
() -> new Time( System.currentTimeMillis() )
precision -> {
final Clock clock = ClockHelper.forPrecision( precision, 3 );
return () -> new Time( clock.millis() );
}
);
generatorDelegates.put(
GENERATOR_PRODUCERS.put(
Timestamp.class,
() -> new Timestamp( System.currentTimeMillis() )
precision -> {
final Clock clock = ClockHelper.forPrecision( precision, 9 );
return () -> Timestamp.from( clock.instant() );
}
);
generatorDelegates.put(
GENERATOR_PRODUCERS.put(
Instant.class,
Instant::now
precision -> {
final Clock clock = ClockHelper.forPrecision( precision, 9 );
return clock::instant;
}
);
generatorDelegates.put(
GENERATOR_PRODUCERS.put(
LocalDate.class,
LocalDate::now
precision -> LocalDate::now
);
generatorDelegates.put(
GENERATOR_PRODUCERS.put(
LocalDateTime.class,
LocalDateTime::now
precision -> {
final Clock clock = ClockHelper.forPrecision( precision, 9 );
return () -> LocalDateTime.now( clock );
}
);
generatorDelegates.put(
GENERATOR_PRODUCERS.put(
LocalTime.class,
LocalTime::now
precision -> {
final Clock clock = ClockHelper.forPrecision( precision, 9 );
return () -> LocalTime.now( clock );
}
);
generatorDelegates.put(
GENERATOR_PRODUCERS.put(
MonthDay.class,
MonthDay::now
precision -> MonthDay::now
);
generatorDelegates.put(
GENERATOR_PRODUCERS.put(
OffsetDateTime.class,
OffsetDateTime::now
precision -> {
final Clock clock = ClockHelper.forPrecision( precision, 9 );
return () -> OffsetDateTime.now( clock );
}
);
generatorDelegates.put(
GENERATOR_PRODUCERS.put(
OffsetTime.class,
OffsetTime::now
precision -> {
final Clock clock = ClockHelper.forPrecision( precision, 9 );
return () -> OffsetTime.now( clock );
}
);
generatorDelegates.put(
GENERATOR_PRODUCERS.put(
Year.class,
Year::now
precision -> Year::now
);
generatorDelegates.put(
GENERATOR_PRODUCERS.put(
YearMonth.class,
YearMonth::now
precision -> YearMonth::now
);
generatorDelegates.put(
GENERATOR_PRODUCERS.put(
ZonedDateTime.class,
ZonedDateTime::now
precision -> {
final Clock clock = ClockHelper.forPrecision( precision, 9 );
return () -> ZonedDateTime.now( clock );
}
);
}
public CurrentTimestampGeneration(CurrentTimestamp annotation, Member member, GeneratorCreationContext context) {
delegate = getGeneratorDelegate( annotation.source(), member );
delegate = getGeneratorDelegate( annotation.source(), member, context );
eventTypes = annotation.timing() == GenerationTiming.ALWAYS
? fromArray( annotation.event() )
: annotation.timing().getEquivalent().eventTypes();
}
public CurrentTimestampGeneration(CreationTimestamp annotation, Member member, GeneratorCreationContext context) {
delegate = getGeneratorDelegate( annotation.source(), member );
delegate = getGeneratorDelegate( annotation.source(), member, context );
eventTypes = INSERT_ONLY;
}
public CurrentTimestampGeneration(UpdateTimestamp annotation, Member member, GeneratorCreationContext context) {
delegate = getGeneratorDelegate( annotation.source(), member );
delegate = getGeneratorDelegate( annotation.source(), member, context );
eventTypes = INSERT_AND_UPDATE;
}
private static CurrentTimestampGeneratorDelegate getGeneratorDelegate(SourceType source, Member member) {
private static CurrentTimestampGeneratorDelegate getGeneratorDelegate(
SourceType source,
Member member,
GeneratorCreationContext context) {
return getGeneratorDelegate( source, ReflectHelper.getPropertyType( member ), context );
}
static CurrentTimestampGeneratorDelegate getGeneratorDelegate(
SourceType source,
Class<?> propertyType,
GeneratorCreationContext context) {
switch (source) {
case VM:
// Generator is only used for in-VM generation
return generatorDelegates.get( ReflectHelper.getPropertyType( member ) );
final BasicValue basicValue = (BasicValue) context.getProperty().getValue();
final Size size = basicValue.getColumns().get( 0 ).getColumnSize(
context.getDatabase().getDialect(),
basicValue.getMetadata()
);
final Key key = new Key( propertyType, size.getPrecision() == null ? 0 : size.getPrecision() );
final CurrentTimestampGeneratorDelegate delegate = GENERATOR_DELEGATES.get( key );
if ( delegate != null ) {
return delegate;
}
final IntFunction<CurrentTimestampGeneratorDelegate> producer = GENERATOR_PRODUCERS.get( key.clazz );
if ( producer == null ) {
return null;
}
final CurrentTimestampGeneratorDelegate generatorDelegate = producer.apply( key.precision );
final CurrentTimestampGeneratorDelegate old = GENERATOR_DELEGATES.putIfAbsent(
key,
generatorDelegate
);
return old != null ? old : generatorDelegate;
case DB:
return null;
default:
@ -193,8 +260,42 @@ public class CurrentTimestampGeneration implements BeforeExecutionGenerator, OnE
return new String[] { dialect.currentTimestamp() };
}
private interface CurrentTimestampGeneratorDelegate {
interface CurrentTimestampGeneratorDelegate {
// Left out the Generator params, they're not used anyway. Since this is purely internal, this can be changed if needed
Object generate();
}
private static class Key {
private final Class<?> clazz;
private final int precision;
public Key(Class<?> clazz, int precision) {
this.clazz = clazz;
this.precision = precision;
}
@Override
public boolean equals(Object o) {
if ( this == o ) {
return true;
}
if ( o == null || getClass() != o.getClass() ) {
return false;
}
Key key = (Key) o;
if ( precision != key.precision ) {
return false;
}
return clazz.equals( key.clazz );
}
@Override
public int hashCode() {
int result = clazz.hashCode();
result = 31 * result + precision;
return result;
}
}
}

View File

@ -6,9 +6,7 @@
*/
package org.hibernate.generator.internal;
import org.hibernate.AssertionFailure;
import org.hibernate.Internal;
import org.hibernate.Session;
import org.hibernate.annotations.Source;
import org.hibernate.annotations.SourceType;
import org.hibernate.dialect.Dialect;
@ -19,8 +17,8 @@ import org.hibernate.generator.EventTypeSets;
import org.hibernate.generator.GeneratorCreationContext;
import org.hibernate.generator.BeforeExecutionGenerator;
import org.hibernate.internal.CoreMessageLogger;
import org.hibernate.tuple.TimestampGenerators;
import org.hibernate.tuple.ValueGenerator;
import org.hibernate.type.descriptor.java.JavaType;
import org.jboss.logging.Logger;
import java.lang.reflect.Member;
@ -59,25 +57,16 @@ public class SourceGeneration implements BeforeExecutionGenerator {
SourceGeneration.class.getName()
);
private final Class<?> propertyType;
private final ValueGenerator<?> valueGenerator;
private final JavaType<?> propertyType;
private final CurrentTimestampGeneration.CurrentTimestampGeneratorDelegate valueGenerator;
public SourceGeneration(Source annotation, Member member, GeneratorCreationContext context) {
this( annotation.value(), context.getProperty().getType().getReturnedClass() );
this( annotation.value(), context.getProperty().getType().getReturnedClass(), context );
}
public SourceGeneration(SourceType sourceType, Class<?> propertyType) {
this.propertyType = propertyType;
switch ( sourceType ) {
case DB:
valueGenerator = this::generateValue;
break;
case VM:
valueGenerator = TimestampGenerators.get( propertyType );
break;
default:
throw new AssertionFailure( "unknown source type" );
}
public SourceGeneration(SourceType sourceType, Class<?> propertyType, GeneratorCreationContext context) {
this.propertyType = context.getDatabase().getTypeConfiguration().getJavaTypeRegistry().getDescriptor( propertyType );
this.valueGenerator = CurrentTimestampGeneration.getGeneratorDelegate( sourceType, propertyType, context );
}
/**
@ -90,13 +79,12 @@ public class SourceGeneration implements BeforeExecutionGenerator {
@Override
public Object generate(SharedSessionContractImplementor session, Object owner, Object currentValue, EventType eventType) {
return valueGenerator.generateValue( session.asSessionImplementor(), owner, currentValue );
}
public Object generateValue(Session session, Object owner) {
SharedSessionContractImplementor implementor = (SharedSessionContractImplementor) session;
return implementor.getTypeConfiguration().getBasicTypeForJavaType( propertyType )
.getJavaTypeDescriptor().wrap( getCurrentTimestamp( implementor ), implementor );
if ( valueGenerator == null ) {
return propertyType.wrap( getCurrentTimestamp( session ), session );
}
else {
return valueGenerator.generate();
}
}
private Timestamp getCurrentTimestamp(SharedSessionContractImplementor session) {

View File

@ -179,6 +179,6 @@ public class CalendarJavaType extends AbstractTemporalJavaType<Calendar> impleme
@Override
public Calendar seed(Long length, Integer precision, Integer scale, SharedSessionContractImplementor session) {
return GregorianCalendar.from( ZonedDateTime.now( ClockHelper.forPrecision( precision, session ) ) );
return GregorianCalendar.from( ZonedDateTime.now( ClockHelper.forPrecision( precision, session, 3 ) ) );
}
}

View File

@ -28,6 +28,10 @@ public class ClockHelper {
private static final Clock TICK_0 = Clock.tick( TICK_9, Duration.ofNanos( 1000000000L ) );
public static Clock forPrecision(Integer precision, SharedSessionContractImplementor session) {
return forPrecision( precision, session, 9 );
}
public static Clock forPrecision(Integer precision, SharedSessionContractImplementor session, int maxPrecision) {
final int resolvedPrecision;
if ( precision == null ) {
resolvedPrecision = session.getJdbcServices().getDialect().getDefaultTimestampPrecision();
@ -35,7 +39,11 @@ public class ClockHelper {
else {
resolvedPrecision = precision;
}
switch ( resolvedPrecision ) {
return forPrecision( resolvedPrecision, maxPrecision );
}
public static Clock forPrecision(int resolvedPrecision, int maxPrecision) {
switch ( Math.min( resolvedPrecision, maxPrecision ) ) {
case 0:
return TICK_0;
case 1:

View File

@ -12,6 +12,7 @@ import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import org.hibernate.HibernateError;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;
@ -47,13 +48,9 @@ public class InVmGenerationsWithAnnotationsWithSqlDateTests {
assertThat( saved.lastUpdatedOn ).isNotNull();
saved.name = "changed";
// Let's sleep a millisecond to make sure we actually generate a different timestamp
try {
Thread.sleep( 1L );
}
catch (InterruptedException e) {
// Ignore
}
//We need to wait a little to make sure the timestamps produced are different
waitALittle();
// then changing
final AuditedEntity merged = scope.fromTransaction( session, (s) -> {
@ -101,4 +98,13 @@ public class InVmGenerationsWithAnnotationsWithSqlDateTests {
this.name = name;
}
}
private static void waitALittle() {
try {
Thread.sleep( 10 );
}
catch (InterruptedException e) {
throw new HibernateError( "Unexpected wakeup from test sleep" );
}
}
}

View File

@ -14,6 +14,7 @@ import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import org.hibernate.HibernateError;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;
@ -49,13 +50,9 @@ public class InVmGenerationsWithMultipleAnnotationsTests {
assertThat( saved.lastUpdatedOn ).isNotNull();
saved.name = "changed";
// Let's sleep a millisecond to make sure we actually generate a different timestamp
try {
Thread.sleep( 1L );
}
catch (InterruptedException e) {
// Ignore
}
//We need to wait a little to make sure the timestamps produced are different
waitALittle();
// then changing
final AuditedEntity merged = scope.fromTransaction( session, (s) -> {
@ -113,4 +110,13 @@ public class InVmGenerationsWithMultipleAnnotationsTests {
this.name = name;
}
}
private static void waitALittle() {
try {
Thread.sleep( 10 );
}
catch (InterruptedException e) {
throw new HibernateError( "Unexpected wakeup from test sleep" );
}
}
}

View File

@ -6,6 +6,7 @@
*/
package org.hibernate.orm.test.tenantid;
import org.hibernate.HibernateError;
import org.hibernate.PropertyValueException;
import org.hibernate.boot.SessionFactoryBuilder;
import org.hibernate.boot.spi.MetadataImplementor;
@ -141,11 +142,10 @@ public class TenantIdTest implements SessionFactoryProducer {
scope.inTransaction( s -> s.persist( record ) );
assertEquals( "mine", record.state.tenantId );
assertNotNull( record.state.updated );
// Round the temporal to avoid issues when the VM produces nanosecond precision timestamps
record.state.updated = DateTimeUtils.roundToDefaultPrecision(
record.state.updated,
scope.getSessionFactory().getJdbcServices().getDialect()
);
//We need to wait a little to make sure the timestamps produced are different
waitALittle();
scope.inTransaction( s -> {
Record r = s.find( Record.class, record.id );
assertEquals( "mine", r.state.tenantId );
@ -160,4 +160,13 @@ public class TenantIdTest implements SessionFactoryProducer {
assertEquals( true, r.state.deleted );
} );
}
private static void waitALittle() {
try {
Thread.sleep( 10 );
}
catch (InterruptedException e) {
throw new HibernateError( "Unexpected wakeup from test sleep" );
}
}
}