mirror of
https://github.com/hibernate/hibernate-orm
synced 2025-02-17 00:24:57 +00:00
HHH-15679 proposed fix to OffsetTime handling
The idea is: convert all OffsetTimes to the system offset before sending them on
This commit is contained in:
parent
39f85a2dca
commit
5dfb90bb73
@ -11,10 +11,10 @@
|
|||||||
import java.sql.Types;
|
import java.sql.Types;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
|
import java.time.LocalTime;
|
||||||
import java.time.OffsetDateTime;
|
import java.time.OffsetDateTime;
|
||||||
import java.time.OffsetTime;
|
import java.time.OffsetTime;
|
||||||
import java.time.ZoneOffset;
|
import java.time.ZoneOffset;
|
||||||
import java.time.ZonedDateTime;
|
|
||||||
import java.time.format.DateTimeFormatter;
|
import java.time.format.DateTimeFormatter;
|
||||||
import java.util.Calendar;
|
import java.util.Calendar;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
@ -76,31 +76,46 @@ public <X> X unwrap(OffsetTime offsetTime, Class<X> type, WrapperOptions options
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// for java.time types, we assume that the JDBC timezone, if any, is ignored
|
||||||
|
// (since PS.setObject() doesn't support passing a timezone)
|
||||||
|
|
||||||
if ( OffsetTime.class.isAssignableFrom( type ) ) {
|
if ( OffsetTime.class.isAssignableFrom( type ) ) {
|
||||||
return (X) offsetTime;
|
return (X) offsetTime;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ( Time.class.isAssignableFrom( type ) ) {
|
if ( LocalTime.class.isAssignableFrom( type ) ) {
|
||||||
return (X) Time.valueOf( offsetTime.toLocalTime() );
|
return (X) offsetTime.withOffsetSameInstant( getCurrentSystemOffset() ).toLocalTime();
|
||||||
}
|
}
|
||||||
|
|
||||||
final ZonedDateTime zonedDateTime = offsetTime.atDate( LocalDate.of( 1970, 1, 1 ) ).toZonedDateTime();
|
// for legacy types, we assume that the JDBC timezone is passed to JDBC
|
||||||
|
// (since PS.setTime() and friends do accept a timezone passed as a Calendar)
|
||||||
|
|
||||||
|
final OffsetTime jdbcOffsetTime = offsetTime.withOffsetSameInstant( getCurrentJdbcOffset(options) );
|
||||||
|
|
||||||
|
if ( Time.class.isAssignableFrom( type ) ) {
|
||||||
|
return (X) Time.valueOf( jdbcOffsetTime.toLocalTime() );
|
||||||
|
}
|
||||||
|
|
||||||
|
final OffsetDateTime jdbcOffsetDateTime = jdbcOffsetTime.atDate( LocalDate.EPOCH );
|
||||||
|
|
||||||
if ( Timestamp.class.isAssignableFrom( type ) ) {
|
if ( Timestamp.class.isAssignableFrom( type ) ) {
|
||||||
/*
|
/*
|
||||||
* Workaround for HHH-13266 (JDK-8061577).
|
* Workaround for HHH-13266 (JDK-8061577).
|
||||||
* Ideally we'd want to use Timestamp.from( offsetDateTime.toInstant() ), but this won't always work.
|
* Ideally we'd want to use Timestamp.from( jdbcOffsetDateTime.toInstant() ),
|
||||||
* Timestamp.from() assumes the number of milliseconds since the epoch
|
* but this won't always work since Timestamp.from() assumes the number of
|
||||||
* means the same thing in Timestamp and Instant, but it doesn't, in particular before 1900.
|
* milliseconds since the epoch means the same thing in Timestamp and Instant,
|
||||||
|
* but it doesn't, in particular before 1900.
|
||||||
*/
|
*/
|
||||||
return (X) Timestamp.valueOf( zonedDateTime.toLocalDateTime() );
|
return (X) Timestamp.valueOf( jdbcOffsetDateTime.toLocalDateTime() );
|
||||||
}
|
}
|
||||||
|
|
||||||
if ( Calendar.class.isAssignableFrom( type ) ) {
|
if ( Calendar.class.isAssignableFrom( type ) ) {
|
||||||
return (X) GregorianCalendar.from( zonedDateTime );
|
return (X) GregorianCalendar.from( jdbcOffsetDateTime.toZonedDateTime() );
|
||||||
}
|
}
|
||||||
|
|
||||||
final Instant instant = zonedDateTime.toInstant();
|
// for instants, we assume that the JDBC timezone, if any, is ignored
|
||||||
|
|
||||||
|
final Instant instant = offsetTime.atDate( LocalDate.EPOCH ).toInstant();
|
||||||
|
|
||||||
if ( Long.class.isAssignableFrom( type ) ) {
|
if ( Long.class.isAssignableFrom( type ) ) {
|
||||||
return (X) Long.valueOf( instant.toEpochMilli() );
|
return (X) Long.valueOf( instant.toEpochMilli() );
|
||||||
@ -119,10 +134,17 @@ public <X> OffsetTime wrap(X value, WrapperOptions options) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// for java.time types, we assume that the JDBC timezone, if any, is ignored
|
||||||
|
// (since PS.setObject() doesn't support passing a timezone)
|
||||||
|
|
||||||
if (value instanceof OffsetTime) {
|
if (value instanceof OffsetTime) {
|
||||||
return (OffsetTime) value;
|
return (OffsetTime) value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (value instanceof LocalTime) {
|
||||||
|
return ((LocalTime) value).atOffset( getCurrentSystemOffset() );
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Also, in order to fix HHH-13357, and to be consistent with the conversion to Time (see above),
|
* Also, in order to fix HHH-13357, and to be consistent with the conversion to Time (see above),
|
||||||
* we set the offset to the current offset of the JVM (OffsetDateTime.now().getOffset()).
|
* we set the offset to the current offset of the JVM (OffsetDateTime.now().getOffset()).
|
||||||
@ -137,30 +159,39 @@ public <X> OffsetTime wrap(X value, WrapperOptions options) {
|
|||||||
* Of course none of this would be a problem if we just stored the offset in the database,
|
* Of course none of this would be a problem if we just stored the offset in the database,
|
||||||
* but I guess there are historical reasons that explain why we don't.
|
* but I guess there are historical reasons that explain why we don't.
|
||||||
*/
|
*/
|
||||||
ZoneOffset offset = OffsetDateTime.now().getOffset();
|
|
||||||
|
// for legacy types, we assume that the JDBC timezone is passed to JDBC
|
||||||
|
// (since PS.setTime() and friends do accept a timezone passed as a Calendar)
|
||||||
|
|
||||||
if (value instanceof Time) {
|
if (value instanceof Time) {
|
||||||
return ( (Time) value ).toLocalTime().atOffset( offset );
|
final Time time = (Time) value;
|
||||||
|
return time.toLocalTime().atOffset( getCurrentJdbcOffset(options) )
|
||||||
|
.withOffsetSameInstant( getCurrentSystemOffset() );
|
||||||
}
|
}
|
||||||
|
|
||||||
if (value instanceof Timestamp) {
|
if (value instanceof Timestamp) {
|
||||||
final Timestamp ts = (Timestamp) value;
|
final Timestamp ts = (Timestamp) value;
|
||||||
/*
|
/*
|
||||||
* Workaround for HHH-13266 (JDK-8061577).
|
* Workaround for HHH-13266 (JDK-8061577).
|
||||||
* Ideally we'd want to use OffsetDateTime.ofInstant( ts.toInstant(), ... ), but this won't always work.
|
* Ideally we'd want to use OffsetDateTime.ofInstant( ts.toInstant(), ... ),
|
||||||
* ts.toInstant() assumes the number of milliseconds since the epoch
|
* but this won't always work since ts.toInstant() assumes the number of
|
||||||
* means the same thing in Timestamp and Instant, but it doesn't, in particular before 1900.
|
* milliseconds since the epoch means the same thing in Timestamp and Instant,
|
||||||
|
* but it doesn't, in particular before 1900.
|
||||||
*/
|
*/
|
||||||
return ts.toLocalDateTime().toLocalTime().atOffset( offset );
|
return ts.toLocalDateTime().toLocalTime().atOffset( getCurrentJdbcOffset(options) )
|
||||||
|
.withOffsetSameInstant( getCurrentSystemOffset() );
|
||||||
}
|
}
|
||||||
|
|
||||||
if (value instanceof Date) {
|
if (value instanceof Date) {
|
||||||
final Date date = (Date) value;
|
final Date date = (Date) value;
|
||||||
return OffsetTime.ofInstant( date.toInstant(), offset );
|
return OffsetTime.ofInstant( date.toInstant(), getCurrentSystemOffset() );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// for instants, we assume that the JDBC timezone, if any, is ignored
|
||||||
|
|
||||||
if (value instanceof Long) {
|
if (value instanceof Long) {
|
||||||
return OffsetTime.ofInstant( Instant.ofEpochMilli( (Long) value ), offset );
|
final long millis = (Long) value;
|
||||||
|
return OffsetTime.ofInstant( Instant.ofEpochMilli(millis), getCurrentSystemOffset() );
|
||||||
}
|
}
|
||||||
|
|
||||||
if (value instanceof Calendar) {
|
if (value instanceof Calendar) {
|
||||||
@ -171,6 +202,19 @@ public <X> OffsetTime wrap(X value, WrapperOptions options) {
|
|||||||
throw unknownWrap( value.getClass() );
|
throw unknownWrap( value.getClass() );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static ZoneOffset getCurrentJdbcOffset(WrapperOptions options) {
|
||||||
|
if ( options.getJdbcTimeZone() != null ) {
|
||||||
|
return OffsetDateTime.now().atZoneSameInstant( options.getJdbcTimeZone().toZoneId() ).getOffset();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return getCurrentSystemOffset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ZoneOffset getCurrentSystemOffset() {
|
||||||
|
return OffsetDateTime.now().getOffset();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getDefaultSqlPrecision(Dialect dialect, JdbcType jdbcType) {
|
public int getDefaultSqlPrecision(Dialect dialect, JdbcType jdbcType) {
|
||||||
return 0;
|
return 0;
|
||||||
|
@ -25,7 +25,6 @@
|
|||||||
import org.hibernate.boot.model.TypeContributions;
|
import org.hibernate.boot.model.TypeContributions;
|
||||||
import org.hibernate.cfg.AvailableSettings;
|
import org.hibernate.cfg.AvailableSettings;
|
||||||
import org.hibernate.cfg.Configuration;
|
import org.hibernate.cfg.Configuration;
|
||||||
import org.hibernate.dialect.DatabaseVersion;
|
|
||||||
import org.hibernate.dialect.Dialect;
|
import org.hibernate.dialect.Dialect;
|
||||||
import org.hibernate.dialect.H2Dialect;
|
import org.hibernate.dialect.H2Dialect;
|
||||||
import org.hibernate.service.ServiceRegistry;
|
import org.hibernate.service.ServiceRegistry;
|
||||||
@ -125,9 +124,10 @@ public void writeThenRead() {
|
|||||||
} );
|
} );
|
||||||
inTransaction( session -> {
|
inTransaction( session -> {
|
||||||
T read = getActualPropertyValue( session.find( getEntityType(), 1 ) );
|
T read = getActualPropertyValue( session.find( getEntityType(), 1 ) );
|
||||||
|
T expected = getExpectedPropertyValueAfterHibernateRead();
|
||||||
assertEquals(
|
assertEquals(
|
||||||
"Writing then reading a value should return the original value",
|
"Writing then reading a value should return the original value",
|
||||||
getExpectedPropertyValueAfterHibernateRead(), read
|
expected, read
|
||||||
);
|
);
|
||||||
} );
|
} );
|
||||||
} );
|
} );
|
||||||
@ -152,10 +152,10 @@ public void writeThenNativeRead() {
|
|||||||
try (ResultSet resultSet = statement.getResultSet()) {
|
try (ResultSet resultSet = statement.getResultSet()) {
|
||||||
resultSet.next();
|
resultSet.next();
|
||||||
Object nativeRead = getActualJdbcValue( resultSet, 1 );
|
Object nativeRead = getActualJdbcValue( resultSet, 1 );
|
||||||
|
Object expected = getExpectedJdbcValueAfterHibernateWrite();
|
||||||
assertEquals(
|
assertEquals(
|
||||||
"Values written by Hibernate ORM should match the original value (same day, hour, ...)",
|
"Values written by Hibernate ORM should match the original value (same day, hour, ...)",
|
||||||
getExpectedJdbcValueAfterHibernateWrite(),
|
expected, nativeRead
|
||||||
nativeRead
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -184,9 +184,10 @@ public void nativeWriteThenRead() {
|
|||||||
} );
|
} );
|
||||||
inTransaction( session -> {
|
inTransaction( session -> {
|
||||||
T read = getActualPropertyValue( session.find( getEntityType(), 1 ) );
|
T read = getActualPropertyValue( session.find( getEntityType(), 1 ) );
|
||||||
|
T expected = getExpectedPropertyValueAfterHibernateRead();
|
||||||
assertEquals(
|
assertEquals(
|
||||||
"Values written without Hibernate ORM should be read correctly by Hibernate ORM",
|
"Values written without Hibernate ORM should be read correctly by Hibernate ORM",
|
||||||
getExpectedPropertyValueAfterHibernateRead(), read
|
expected, read
|
||||||
);
|
);
|
||||||
} );
|
} );
|
||||||
} );
|
} );
|
||||||
|
@ -12,6 +12,7 @@
|
|||||||
import java.sql.Time;
|
import java.sql.Time;
|
||||||
import java.sql.Timestamp;
|
import java.sql.Timestamp;
|
||||||
import java.sql.Types;
|
import java.sql.Types;
|
||||||
|
import java.time.LocalDate;
|
||||||
import java.time.OffsetDateTime;
|
import java.time.OffsetDateTime;
|
||||||
import java.time.OffsetTime;
|
import java.time.OffsetTime;
|
||||||
import java.time.ZoneId;
|
import java.time.ZoneId;
|
||||||
@ -19,6 +20,7 @@
|
|||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import jakarta.persistence.Basic;
|
import jakarta.persistence.Basic;
|
||||||
import jakarta.persistence.Column;
|
import jakarta.persistence.Column;
|
||||||
import jakarta.persistence.Entity;
|
import jakarta.persistence.Entity;
|
||||||
@ -173,11 +175,11 @@ protected OffsetTime getOriginalPropertyValue() {
|
|||||||
protected OffsetTime getExpectedPropertyValueAfterHibernateRead() {
|
protected OffsetTime getExpectedPropertyValueAfterHibernateRead() {
|
||||||
// For some reason, the offset is not stored, so the restored values use the offset from the default JVM timezone.
|
// For some reason, the offset is not stored, so the restored values use the offset from the default JVM timezone.
|
||||||
if ( TimeAsTimestampRemappingH2Dialect.class.equals( getRemappingDialectClass() ) ) {
|
if ( TimeAsTimestampRemappingH2Dialect.class.equals( getRemappingDialectClass() ) ) {
|
||||||
return getOriginalPropertyValue().withOffsetSameLocal( OffsetDateTime.now().getOffset() );
|
return getOriginalPropertyValue().withOffsetSameInstant( OffsetDateTime.now().getOffset() );
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// When storing time as java.sql.Time, we only get second precision (not nanosecond)
|
// When storing time as java.sql.Time, we only get second precision (not nanosecond)
|
||||||
return getOriginalPropertyValue().withNano( 0 ).withOffsetSameLocal( OffsetDateTime.now().getOffset() );
|
return getOriginalPropertyValue().withNano( 0 ).withOffsetSameInstant( OffsetDateTime.now().getOffset() );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -189,29 +191,28 @@ protected OffsetTime getActualPropertyValue(EntityWithOffsetTime entity) {
|
|||||||
@Override
|
@Override
|
||||||
protected void setJdbcValueForNonHibernateWrite(PreparedStatement statement, int parameterIndex)
|
protected void setJdbcValueForNonHibernateWrite(PreparedStatement statement, int parameterIndex)
|
||||||
throws SQLException {
|
throws SQLException {
|
||||||
|
OffsetTime offsetTime = OffsetTime.of( hour, minute, second, 0, ZoneOffset.of(offset) )
|
||||||
|
.withOffsetSameInstant( OffsetDateTime.now().getOffset() );
|
||||||
if ( TimeAsTimestampRemappingH2Dialect.class.equals( getRemappingDialectClass() ) ) {
|
if ( TimeAsTimestampRemappingH2Dialect.class.equals( getRemappingDialectClass() ) ) {
|
||||||
statement.setTimestamp(
|
statement.setTimestamp(
|
||||||
parameterIndex,
|
parameterIndex,
|
||||||
new Timestamp(
|
Timestamp.valueOf( offsetTime.atDate( LocalDate.EPOCH ).toLocalDateTime() )
|
||||||
yearWhenPersistedWithoutHibernate - 1900,
|
|
||||||
monthWhenPersistedWithoutHibernate - 1,
|
|
||||||
dayWhenPersistedWithoutHibernate,
|
|
||||||
hour, minute, second, nanosecond
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
statement.setTime( parameterIndex, new Time( hour, minute, second ) );
|
statement.setTime( parameterIndex, Time.valueOf( offsetTime.toLocalTime() ) );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected Object getExpectedJdbcValueAfterHibernateWrite() {
|
protected Object getExpectedJdbcValueAfterHibernateWrite() {
|
||||||
|
OffsetTime offsetTime = OffsetTime.of( hour, minute, second, 0, ZoneOffset.of(offset) )
|
||||||
|
.withOffsetSameInstant( OffsetDateTime.now().getOffset() );
|
||||||
if ( TimeAsTimestampRemappingH2Dialect.class.equals( getRemappingDialectClass() ) ) {
|
if ( TimeAsTimestampRemappingH2Dialect.class.equals( getRemappingDialectClass() ) ) {
|
||||||
return new Timestamp( 1970 - 1900, 0, 1, hour, minute, second, nanosecond );
|
return Timestamp.valueOf( offsetTime.atDate( LocalDate.EPOCH ).toLocalDateTime() );
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
return new Time( hour, minute, second );
|
return Time.valueOf( offsetTime.toLocalTime() );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user