Respect precision for VM generated temporal values
This commit is contained in:
parent
dfa403b8f4
commit
f68ea21891
|
@ -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 );
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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 ) ) );
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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" );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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" );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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" );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue