Skip to content

Commit 5dfb90b

Browse files
committed
HHH-15679 proposed fix to OffsetTime handling
The idea is: convert all OffsetTimes to the system offset before sending them on
1 parent 39f85a2 commit 5dfb90b

File tree

3 files changed

+79
-33
lines changed

3 files changed

+79
-33
lines changed

hibernate-core/src/main/java/org/hibernate/type/descriptor/java/OffsetTimeJavaType.java

Lines changed: 61 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,10 @@
1111
import java.sql.Types;
1212
import java.time.Instant;
1313
import java.time.LocalDate;
14+
import java.time.LocalTime;
1415
import java.time.OffsetDateTime;
1516
import java.time.OffsetTime;
1617
import java.time.ZoneOffset;
17-
import java.time.ZonedDateTime;
1818
import java.time.format.DateTimeFormatter;
1919
import java.util.Calendar;
2020
import java.util.Date;
@@ -76,31 +76,46 @@ public <X> X unwrap(OffsetTime offsetTime, Class<X> type, WrapperOptions options
7676
return null;
7777
}
7878

79+
// for java.time types, we assume that the JDBC timezone, if any, is ignored
80+
// (since PS.setObject() doesn't support passing a timezone)
81+
7982
if ( OffsetTime.class.isAssignableFrom( type ) ) {
8083
return (X) offsetTime;
8184
}
8285

86+
if ( LocalTime.class.isAssignableFrom( type ) ) {
87+
return (X) offsetTime.withOffsetSameInstant( getCurrentSystemOffset() ).toLocalTime();
88+
}
89+
90+
// for legacy types, we assume that the JDBC timezone is passed to JDBC
91+
// (since PS.setTime() and friends do accept a timezone passed as a Calendar)
92+
93+
final OffsetTime jdbcOffsetTime = offsetTime.withOffsetSameInstant( getCurrentJdbcOffset(options) );
94+
8395
if ( Time.class.isAssignableFrom( type ) ) {
84-
return (X) Time.valueOf( offsetTime.toLocalTime() );
96+
return (X) Time.valueOf( jdbcOffsetTime.toLocalTime() );
8597
}
8698

87-
final ZonedDateTime zonedDateTime = offsetTime.atDate( LocalDate.of( 1970, 1, 1 ) ).toZonedDateTime();
99+
final OffsetDateTime jdbcOffsetDateTime = jdbcOffsetTime.atDate( LocalDate.EPOCH );
88100

89101
if ( Timestamp.class.isAssignableFrom( type ) ) {
90102
/*
91103
* Workaround for HHH-13266 (JDK-8061577).
92-
* Ideally we'd want to use Timestamp.from( offsetDateTime.toInstant() ), but this won't always work.
93-
* Timestamp.from() assumes the number of milliseconds since the epoch
94-
* means the same thing in Timestamp and Instant, but it doesn't, in particular before 1900.
104+
* Ideally we'd want to use Timestamp.from( jdbcOffsetDateTime.toInstant() ),
105+
* but this won't always work since Timestamp.from() assumes the number of
106+
* milliseconds since the epoch means the same thing in Timestamp and Instant,
107+
* but it doesn't, in particular before 1900.
95108
*/
96-
return (X) Timestamp.valueOf( zonedDateTime.toLocalDateTime() );
109+
return (X) Timestamp.valueOf( jdbcOffsetDateTime.toLocalDateTime() );
97110
}
98111

99112
if ( Calendar.class.isAssignableFrom( type ) ) {
100-
return (X) GregorianCalendar.from( zonedDateTime );
113+
return (X) GregorianCalendar.from( jdbcOffsetDateTime.toZonedDateTime() );
101114
}
102115

103-
final Instant instant = zonedDateTime.toInstant();
116+
// for instants, we assume that the JDBC timezone, if any, is ignored
117+
118+
final Instant instant = offsetTime.atDate( LocalDate.EPOCH ).toInstant();
104119

105120
if ( Long.class.isAssignableFrom( type ) ) {
106121
return (X) Long.valueOf( instant.toEpochMilli() );
@@ -119,10 +134,17 @@ public <X> OffsetTime wrap(X value, WrapperOptions options) {
119134
return null;
120135
}
121136

137+
// for java.time types, we assume that the JDBC timezone, if any, is ignored
138+
// (since PS.setObject() doesn't support passing a timezone)
139+
122140
if (value instanceof OffsetTime) {
123141
return (OffsetTime) value;
124142
}
125143

144+
if (value instanceof LocalTime) {
145+
return ((LocalTime) value).atOffset( getCurrentSystemOffset() );
146+
}
147+
126148
/*
127149
* Also, in order to fix HHH-13357, and to be consistent with the conversion to Time (see above),
128150
* 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) {
137159
* Of course none of this would be a problem if we just stored the offset in the database,
138160
* but I guess there are historical reasons that explain why we don't.
139161
*/
140-
ZoneOffset offset = OffsetDateTime.now().getOffset();
162+
163+
// for legacy types, we assume that the JDBC timezone is passed to JDBC
164+
// (since PS.setTime() and friends do accept a timezone passed as a Calendar)
141165

142166
if (value instanceof Time) {
143-
return ( (Time) value ).toLocalTime().atOffset( offset );
167+
final Time time = (Time) value;
168+
return time.toLocalTime().atOffset( getCurrentJdbcOffset(options) )
169+
.withOffsetSameInstant( getCurrentSystemOffset() );
144170
}
145171

146172
if (value instanceof Timestamp) {
147173
final Timestamp ts = (Timestamp) value;
148174
/*
149175
* Workaround for HHH-13266 (JDK-8061577).
150-
* Ideally we'd want to use OffsetDateTime.ofInstant( ts.toInstant(), ... ), but this won't always work.
151-
* ts.toInstant() assumes the number of milliseconds since the epoch
152-
* means the same thing in Timestamp and Instant, but it doesn't, in particular before 1900.
176+
* Ideally we'd want to use OffsetDateTime.ofInstant( ts.toInstant(), ... ),
177+
* but this won't always work since ts.toInstant() assumes the number of
178+
* milliseconds since the epoch means the same thing in Timestamp and Instant,
179+
* but it doesn't, in particular before 1900.
153180
*/
154-
return ts.toLocalDateTime().toLocalTime().atOffset( offset );
181+
return ts.toLocalDateTime().toLocalTime().atOffset( getCurrentJdbcOffset(options) )
182+
.withOffsetSameInstant( getCurrentSystemOffset() );
155183
}
156184

157185
if (value instanceof Date) {
158186
final Date date = (Date) value;
159-
return OffsetTime.ofInstant( date.toInstant(), offset );
187+
return OffsetTime.ofInstant( date.toInstant(), getCurrentSystemOffset() );
160188
}
161189

190+
// for instants, we assume that the JDBC timezone, if any, is ignored
191+
162192
if (value instanceof Long) {
163-
return OffsetTime.ofInstant( Instant.ofEpochMilli( (Long) value ), offset );
193+
final long millis = (Long) value;
194+
return OffsetTime.ofInstant( Instant.ofEpochMilli(millis), getCurrentSystemOffset() );
164195
}
165196

166197
if (value instanceof Calendar) {
@@ -171,6 +202,19 @@ public <X> OffsetTime wrap(X value, WrapperOptions options) {
171202
throw unknownWrap( value.getClass() );
172203
}
173204

205+
private static ZoneOffset getCurrentJdbcOffset(WrapperOptions options) {
206+
if ( options.getJdbcTimeZone() != null ) {
207+
return OffsetDateTime.now().atZoneSameInstant( options.getJdbcTimeZone().toZoneId() ).getOffset();
208+
}
209+
else {
210+
return getCurrentSystemOffset();
211+
}
212+
}
213+
214+
private static ZoneOffset getCurrentSystemOffset() {
215+
return OffsetDateTime.now().getOffset();
216+
}
217+
174218
@Override
175219
public int getDefaultSqlPrecision(Dialect dialect, JdbcType jdbcType) {
176220
return 0;

hibernate-core/src/test/java/org/hibernate/orm/test/type/AbstractJavaTimeTypeTest.java

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@
2525
import org.hibernate.boot.model.TypeContributions;
2626
import org.hibernate.cfg.AvailableSettings;
2727
import org.hibernate.cfg.Configuration;
28-
import org.hibernate.dialect.DatabaseVersion;
2928
import org.hibernate.dialect.Dialect;
3029
import org.hibernate.dialect.H2Dialect;
3130
import org.hibernate.service.ServiceRegistry;
@@ -125,9 +124,10 @@ public void writeThenRead() {
125124
} );
126125
inTransaction( session -> {
127126
T read = getActualPropertyValue( session.find( getEntityType(), 1 ) );
127+
T expected = getExpectedPropertyValueAfterHibernateRead();
128128
assertEquals(
129129
"Writing then reading a value should return the original value",
130-
getExpectedPropertyValueAfterHibernateRead(), read
130+
expected, read
131131
);
132132
} );
133133
} );
@@ -152,10 +152,10 @@ public void writeThenNativeRead() {
152152
try (ResultSet resultSet = statement.getResultSet()) {
153153
resultSet.next();
154154
Object nativeRead = getActualJdbcValue( resultSet, 1 );
155+
Object expected = getExpectedJdbcValueAfterHibernateWrite();
155156
assertEquals(
156157
"Values written by Hibernate ORM should match the original value (same day, hour, ...)",
157-
getExpectedJdbcValueAfterHibernateWrite(),
158-
nativeRead
158+
expected, nativeRead
159159
);
160160
}
161161
}
@@ -184,9 +184,10 @@ public void nativeWriteThenRead() {
184184
} );
185185
inTransaction( session -> {
186186
T read = getActualPropertyValue( session.find( getEntityType(), 1 ) );
187+
T expected = getExpectedPropertyValueAfterHibernateRead();
187188
assertEquals(
188189
"Values written without Hibernate ORM should be read correctly by Hibernate ORM",
189-
getExpectedPropertyValueAfterHibernateRead(), read
190+
expected, read
190191
);
191192
} );
192193
} );

hibernate-core/src/test/java/org/hibernate/orm/test/type/OffsetTimeTest.java

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,15 @@
1212
import java.sql.Time;
1313
import java.sql.Timestamp;
1414
import java.sql.Types;
15+
import java.time.LocalDate;
1516
import java.time.OffsetDateTime;
1617
import java.time.OffsetTime;
1718
import java.time.ZoneId;
1819
import java.time.ZoneOffset;
1920
import java.util.Arrays;
2021
import java.util.Collections;
2122
import java.util.List;
23+
2224
import jakarta.persistence.Basic;
2325
import jakarta.persistence.Column;
2426
import jakarta.persistence.Entity;
@@ -173,11 +175,11 @@ protected OffsetTime getOriginalPropertyValue() {
173175
protected OffsetTime getExpectedPropertyValueAfterHibernateRead() {
174176
// For some reason, the offset is not stored, so the restored values use the offset from the default JVM timezone.
175177
if ( TimeAsTimestampRemappingH2Dialect.class.equals( getRemappingDialectClass() ) ) {
176-
return getOriginalPropertyValue().withOffsetSameLocal( OffsetDateTime.now().getOffset() );
178+
return getOriginalPropertyValue().withOffsetSameInstant( OffsetDateTime.now().getOffset() );
177179
}
178180
else {
179181
// When storing time as java.sql.Time, we only get second precision (not nanosecond)
180-
return getOriginalPropertyValue().withNano( 0 ).withOffsetSameLocal( OffsetDateTime.now().getOffset() );
182+
return getOriginalPropertyValue().withNano( 0 ).withOffsetSameInstant( OffsetDateTime.now().getOffset() );
181183
}
182184
}
183185

@@ -189,29 +191,28 @@ protected OffsetTime getActualPropertyValue(EntityWithOffsetTime entity) {
189191
@Override
190192
protected void setJdbcValueForNonHibernateWrite(PreparedStatement statement, int parameterIndex)
191193
throws SQLException {
194+
OffsetTime offsetTime = OffsetTime.of( hour, minute, second, 0, ZoneOffset.of(offset) )
195+
.withOffsetSameInstant( OffsetDateTime.now().getOffset() );
192196
if ( TimeAsTimestampRemappingH2Dialect.class.equals( getRemappingDialectClass() ) ) {
193197
statement.setTimestamp(
194198
parameterIndex,
195-
new Timestamp(
196-
yearWhenPersistedWithoutHibernate - 1900,
197-
monthWhenPersistedWithoutHibernate - 1,
198-
dayWhenPersistedWithoutHibernate,
199-
hour, minute, second, nanosecond
200-
)
199+
Timestamp.valueOf( offsetTime.atDate( LocalDate.EPOCH ).toLocalDateTime() )
201200
);
202201
}
203202
else {
204-
statement.setTime( parameterIndex, new Time( hour, minute, second ) );
203+
statement.setTime( parameterIndex, Time.valueOf( offsetTime.toLocalTime() ) );
205204
}
206205
}
207206

208207
@Override
209208
protected Object getExpectedJdbcValueAfterHibernateWrite() {
209+
OffsetTime offsetTime = OffsetTime.of( hour, minute, second, 0, ZoneOffset.of(offset) )
210+
.withOffsetSameInstant( OffsetDateTime.now().getOffset() );
210211
if ( TimeAsTimestampRemappingH2Dialect.class.equals( getRemappingDialectClass() ) ) {
211-
return new Timestamp( 1970 - 1900, 0, 1, hour, minute, second, nanosecond );
212+
return Timestamp.valueOf( offsetTime.atDate( LocalDate.EPOCH ).toLocalDateTime() );
212213
}
213214
else {
214-
return new Time( hour, minute, second );
215+
return Time.valueOf( offsetTime.toLocalTime() );
215216
}
216217
}
217218

0 commit comments

Comments
 (0)