Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Unwanted Automatic Time Zone Conversion Using Hibernate/JPA and JDK Date

I am using Hibernate (4.2) as my persistence provider, and I have a JPA entity that contains a Date field:

@Entity
@Table(name = "MY_TABLE")
public class MyTable implements Serializable {
  . . .
  @Temporal(TemporalType.TIMESTAMP)
  @Column(name = "START_DATE")
  private Date startDate;
  public Date getStartDate() {
    return startDate;
  }
  public void setStartDate(Date startDate) {
    this.startDate = startDate;
  }
  . . .
}

The column corresponding to START_DATE is defined as START_DATE TIMESTAMP (no time zone).

I am using Joda-Time (2.3) internally to my application to deal with the date (always in UTC), and just prior to persisting the Entity, I use the toDate() method of Joda's DateTime class to get a JDK Date object in order to obey the mapping:

public void myMethod(DateTime startDateUTC) {
  . . .
  MyTable table = /* obtain somehow */
  table.setStartDate(startDateUTC.toDate());
  . . .
}

When I look in the DB at the value that is stored, I notice that somewhere (JDK? Hibernate?) converts the Date value using the default Time Zone of the JVM where the code runs. In my case that is "America/Chicago".

The problem really manifests itself near Daylight Savings Time (DST). For example, if the time internally is

2014-03-09T02:55:00Z

it gets stored as

09-Mar-14 03:55:00

What I would like, is for it to be stored as

09-Mar-14 02:55:00

However, in CDT, 2:55AM on March 9 does not exist ("Spring forward"). So something (JDK? Hibernate?) is rolling the date forward.

I would like for the instant that gets stored in the DB to be in UTC. After all, that's how I am dealing with it internally to my application, but as soon as I hand it off to be persisted, it gets converted to my default time zone.

Note: I am unable to set the default TimeZone using

TimeZone.setDefault(TimeZone.getTimeZone("UTC"))

because the JVM on which I'm running is shared across multiple applications.

How do I store the date in UTC without setting the JVM default Time Zone to UTC?

like image 460
J Steven Perry Avatar asked Mar 05 '14 22:03

J Steven Perry


2 Answers

There is an article about this unexpected time zone shift issue which you can check out here. It provides the explanation for the root of the problem and shows how to deal with it. Of course, the main presumption is that we want to store dates as UTC in the database.

For example, whenever you read a date from the database (let's say: 9:54 UTC), JDBC skips any information about the time zone. So what JVM recieves through JDBC is is a date interpreted as if it was in the local time zone (for my case, 9:54 UTC+2). If the local time zone differs from UTC (and it usually does) we end up with the incorrect time shift.

The similar situation occurs when writing to the DB.

There is a small open source project DbAssist providing fixes for different versions of Hibernate. So if you are using Hibernate 4.2.21 just add the following Maven dependency to your POM file and your problem is solved (the detailed installation instructions of setup with e.g. Spring Boot can be found on the library github).

<dependency>
    <groupId>com.montrosesoftware</groupId>
    <artifactId>DbAssist-4.2.21</artifactId>
    <version>1.0-RELEASE</version>
</dependency>

After applying this fix, your java.util.Date fields in the entity classes will be read and persisted as expected: as if they were stored as UTC in the database. If you are using JPA annotations, you don't have to change the mapping in the entities (as in one of the previous response); it is done automatically.

Internally, the fix uses a custom UTC date type, which overrides Hibernate's date type in order to force it to treat all dates in the DB as UTC. Then, to apply the mapping from java.util.Date to UtcDateType, it uses @Typedef annotation. See below:

@TypeDef(name = "UtcDateType", defaultForType = Date.class, typeClass = UtcDateType.class),
package com.montrosesoftware.dbassist.types;

If your project depends on Hibernate HBM files or other versions of Hibernate, go to project's github wiki for more detailed instructions how to install the proper fix.

like image 38
Smont Avatar answered Oct 25 '22 08:10

Smont


I ran into this myself. What I saw is that even though you've specified UTC as the time zone in your Date (and can see this by printing it out and seeing the 'Z' at the end), for some reason, the JVM wants to take over and convert the date for you using the JVM's default time zone.

Anyway, what you need is a custom mapping to work around this. Try using Jadira:

@Entity
@Table(name = "MY_TABLE")
public class MyTable implements Serializable {
  . . .
  @Column(name = "START_DATE")
  @Type(type="org.jadira.usertype.dateandtime.legacyjdk.PersistentDate")
  private Date startDate;
  public Date getStartDate() {
    return startDate;
  }
  public void setStartDate(Date startDate) {
    this.startDate = startDate;
  }
  . . .
}

By default Jadira's PersistentDate class uses UTC as the time zone when it converts the date to the millisecond value that gets stored in the DB. You can specify other time zones, but it sounds like UTC is what you want to store.

As the comment to your post suggests, sometimes the tool you use to query the DB is doing the mindlessly stupid automatic-what's-my-JDK-default-TZ based conversion for you, leading you to believe the value is still incorrect.

You may try also to store the raw value (as an INTEGER) just to convince yourself that the correct millisecond value is being stored.

HTH,

Mose

like image 119
Schrute Farms Avatar answered Oct 25 '22 07:10

Schrute Farms