[hibernate-dev] Supporting timezone in Timestamp Type

Steve Ebersole steve at hibernate.org
Wed Aug 31 14:51:29 EDT 2016


We discussed this on HipChat, but for the benefit of all on this
discussion...

Part of this (the original report) speaks to a difference in how we map
(org.hibernate.type.Type) java.time temporal versus a java.util temporal -
specifically java.util.Calendar.  When we are passed a Calendar (the model
defines its attribute as Calendar) we pass along that Calendar reference to
the JDBC driver which forces the driver to use that Calendar's TZ.  For
java.time temporal types, we always resolve them to UTC values mainly
because of how we get to the java.sql.Timestamp value from the java.time
value.  For example, for ZonedDateTime we call:

    return (X) Timestamp.from( zonedDateTime.toInstant() );

The call to #toInstant there essentially forces the value to UTC because it
converted directly to epoch-based numeric.

Thinking about this some more and "stepping back", the real problem is that
there are a few different timezones in play here that need to be balanced
and handled:

   1. The database default TZ
   2. The stored TZ
   3. The JVM/JDBC TZ
   4. The application TZ

The whole idea of "stored TZ" really depends on how the database/driver
treats TIMESTAMP and whether it supports a "TIMESTAMP_WITH_TIMEZONE" type.  I
personally think it is a huge mistake to save the specific TZ differences
to the DB, so I would personally continue to not support
TIMESTAMP_WITH_TIMEZONE
types.  This would mean we never have to bind the Calendar because we could
simply convert the value to to the JVM/JDBC TZ ourselves.  Specifically I
would suggest that we (continue to) assume that the driver has been set up
such that the same TZ is used when

   1. when binding a Timestamp (without passing a Calendar)
   2. reading a Timestamp (without passing a Calendar).

Specifically this would mean setting the JVM TZ, or for JDBC drivers that
support it setting the "default TZ" on the driver.  If we start storing
values in the TZ specific to the Calendar we really have no idea how to
properly read those values back because we'd lose context to that TZ.  For
that reason I suggest we drop passing along the Calender and instead
perform all these conversions in memory and pass the converted Timestamp to
JDBC.  When reading back, that should mean the driver would return us
Timestamps relative to that same TZ.

The last piece is "application TZ" which would be an optional setting
indicating the TZ zone-based values should be returned in.

E.g., let's say a user maps a ZonedDataTime using @Temporal(TIMESTAMP).  If
following my suggestions, when saving these values we would:

   1. convert the ZonedDateTime to a ZonedDateTime in the "JVM/JDBC TZ" (if
   different from the incoming ZonedDateTime's TZ)
   2. use that to construct a Timestamp to bind to JDBC

When reading back these values, we would:

   1. get back the Timestamp from JDBC
   2. assume the value is relative to the "JVM/JDBC TZ" and build the
   ZonedDateTime via `ZonedDateTime.ofInstant( timestamp.toInstant(),
   jvmJdbcZoneId )`
   3. convert that built ZonedDateTime to the application TZ:
   `builtZonedDateTime.withZoneSameInstant( applicationZoneId )` - it might
   be possible to combine this step with the previous, not sure...


Some if this is type-specific.  Assuming the suggestions above:

   - Instant - inherently epoch-based.
      - to convert this to a Timestamp for binding we would use
`Timestamp#from(Instant).
      Because of the above assumptions, the driver would interpret
this relative
      to JVM/JDBC TZ
      - Reading back we simply use `Timestamp#toInstant()`
   - LocalDateTime - is the relative idea of a date+time outside the
   context of any specific TZ.
      - When binding we would resolve this into the JVM/JDBC TZ (using
      LocalDateTime#atZone(jvmJdbcZoneId), convert to an Instant and create a
      Timestamp and bind that Timestamp to JDBC.
      - When reading back we'd get the Instant from Timestamp and use that
      in `LocalDateTime#ofInstant` using the application TZ
   - LocalDate - relative idea of a date outside of any specific TZ.  Of
   course this could also be mapped to a JDBC DATE which circumvents some of
   this, but mapped to a TIMESTAMP...
      - When binding we'd convert to a LocalDateTime using
      `LocalDate#atStartOfDay` and treat that LocalDateTime as above.
      - When reading back we would create a LocalDateTime using
`LocalDateTime.ofInstant(timestamp.toInstant(),
      applicationZoneId )`
   - LocalTime - relative idea of a time outside any specific TZ.  Could
   also be mapped to a JDBC TIME...
      - When binding we'd create a LocalDateTime using `LocalTime.atDate(
      LocalDate.of( 1970, 1, 1 ) )`, converting that to a ZonedDateTime using
      `LocalDateTime#atZone( jvmJdbcZoneId )` and finally converting that to a
      Timestamp
      - When reading we would call `LocalDateTime.ofInstant(
      timestamp.toInstant(), applicationZoneId ).toLocalTime()`
   - ZonedDateTime - like a Calendar bundles a date+time with a TZ, and so
   is directly convertible to an epoch-based value (Instant, Timestamp, etc).
      - When binding we would convert this to an Instant and then to a
      Timestamp
      - When reading we would convert the Timestamp to an Instant and then
      convert the Instant to a ZonedDateTime using `ZonedDateTime.ofInstant(
      timestamp.toInstant(), applicationZoneId )`
   - OffsetDateTime - handled similar to ZonedDateTime.
      - When binding we would convert the value to a ZonedDateTime using
      `OffsetDateTime#atZoneSameInstant( jvmJdbcZoneId )` and then follows
      ZonedDateTime
      - When reading we would use `OffsetDateTime.ofInstant(
      timestamp.toInstant(), applicationZoneId )`
   - OffsetTime - atm we just drop the ZoneOffset
   - for java.util.Date and java.sql.Timstamp mapping we have to decide
   what those concepts model in regards to their value relative to a TZ.  Are
   they a LocalDateTime relative to the system TimeZone?  Are they a
   LocalDateTime relative to UTC?  At the moment how we handled these "legacy
   Java temporal types" ends up answering this question as them being "a
   LocalDateTime relative to the system TimeZone".
   - what about java.sql.Time?  Is this a LocalTime?  An OffsetTime
   relative to UTC (+0)?
   - and java.sql.Date?

Lot of moving parts when you actually start looking deep at "supporting
timezones".


[1]
http://stackoverflow.com/questions/18447995/postgresql-9-2-jdbc-driver-uses-client-time-zone,
etc

On Wed, Aug 31, 2016 at 9:10 AM Vlad Mihalcea <mihalcea.vlad at gmail.com>
wrote:

> Hi,
>
> While reviewing the Pull Request for this issue:
> https://github.com/hibernate/hibernate-orm/pull/1536
>
> I realized that we can improve the default TimestampType as follows:
>
> 1. We could make it parameterizable so that it can also take a custom
> timezone (UTC) during mapping.
> 2. We could also define a default timezone so that we don't rely on the JVM
> one. This might be desirable when the UI requires to display the time in a
> certain timezone, while we want to save all timestamps in UTC.
>
> Let me know what you think.
>
> Vlad
> _______________________________________________
> hibernate-dev mailing list
> hibernate-dev at lists.jboss.org
> https://lists.jboss.org/mailman/listinfo/hibernate-dev
>


More information about the hibernate-dev mailing list