How to store date/time and timestamps in UTC time zone with JPA and Hibernate
Asked Answered
D

13

129

How can I configure JPA/Hibernate to store a date/time in the database as UTC (GMT) time zone? Consider this annotated JPA entity:

public class Event {
    @Id
    public int id;

    @Temporal(TemporalType.TIMESTAMP)
    public java.util.Date date;
}

If the date is 2008-Feb-03 9:30am Pacific Standard Time (PST), then I want the UTC time of 2008-Feb-03 5:30pm stored in the database. Likewise, when the date is retrieved from the database, I want it interpreted as UTC. So in this case 530pm is 530pm UTC. When it's displayed it will be formatted as 9:30am PST.

Dmz answered 3/2, 2009 at 17:15 Comment(0)
U
117

Since Hibernate 5.2, you can now force the UTC time zone by adding the following configuration property into the properties.xml JPA configuration file:

<property name="hibernate.jdbc.time_zone" value="UTC"/>

If you're using Spring Boot, then add this property to your application.properties file:

spring.jpa.properties.hibernate.jdbc.time_zone=UTC
Ubiquitous answered 5/11, 2016 at 13:24 Comment(10)
I wrote that one too :D Now, guess who added support for this feature in Hibernate?Ubiquitous
Oh, just now I realized the name and picture are the same in your profile and those articles... Good job Vlad :)Acrodont
@VladMihalcea if it's for Mysql, one need to tell MySql to use timezone by using useTimezone=true in the connection string. Then only setting property hibernate.jdbc.time_zone will workLatinism
Actually, you need to set useLegacyDatetimeCode to falseUbiquitous
hibernate.jdbc.time_zone appears to be ignored or has no effect when used with PostgreSQLBilabiate
Check out my High-Performance Java Persistence GitHub repositor. There's a test case you can run on PostgreSQL and shows that the property works just fine.Ubiquitous
I stored just a date 1985-06-01 in column type of date in postgres. When I retrieve it it gives me 1985-05-31. The timezone are EST for Java and Postgres. Does it mean jpa automatically converts the date using EST?Squatter
If I set this property timezone to utc what would happen to the existing data?Squatter
Adding this to a legacy product requires evaluating whether the existing timestamps won't drift. So, it all depends on how you saved data previously.Ubiquitous
If you use Quarkus, you can make use of quarkus property directly.Winther
B
59

To the best of my knowledge, you need to put your entire Java app in UTC timezone (so that Hibernate will store dates in UTC), and you'll need to convert to whatever timezone desired when you display stuff (at least we do it this way).

At startup, we do:

TimeZone.setDefault(TimeZone.getTimeZone("Etc/UTC"));

And set the desired timezone to the DateFormat:

fmt.setTimeZone(TimeZone.getTimeZone("Europe/Budapest"))
Bozuwa answered 3/2, 2009 at 18:44 Comment(7)
mitchnull, your solution won't work in all cases because Hibernate delegates setting dates to the JDBC driver and each JDBC driver handles dates and time zones differently. see #4124034.Clubfoot
But if I start my app informing to the JVM "-Duser.timezone=+00:00" property, is not the same behave?Illfated
As far as I can tell, it will work in all cases except when the JVM and the database server are in different time zones.Quantifier
stevekuo and @Bozuwa see divestoclimb solution below which is far more better and side-effect proof https://mcmap.net/q/103117/-how-to-store-date-time-and-timestamps-in-utc-time-zone-with-jpa-and-hibernateHelyn
Do HibernateJPA support "@Factory" and "@Externalizer" annotation, it's how I do datetime utc handling in OpenJPA library. #10820362Pokeberry
This does not work when jvm does not use the same time zone as told by @Shane. The simple next response solve all the pb.Gavette
Also add this in ur properties.file to make sure your db entries also happen in UTC spring.jpa.properties.hibernate.jdbc.time_zone=UTCAerate
S
47

Hibernate is ignorant of time zone stuff in Dates (because there isn't any), but it's actually the JDBC layer that's causing problems. ResultSet.getTimestamp and PreparedStatement.setTimestamp both say in their docs that they transform dates to/from the current JVM timezone by default when reading and writing from/to the database.

I came up with a solution to this in Hibernate 3.5 by subclassing org.hibernate.type.TimestampType that forces these JDBC methods to use UTC instead of the local time zone:

public class UtcTimestampType extends TimestampType {

    private static final long serialVersionUID = 8088663383676984635L;

    private static final TimeZone UTC = TimeZone.getTimeZone("UTC");

    @Override
    public Object get(ResultSet rs, String name) throws SQLException {
        return rs.getTimestamp(name, Calendar.getInstance(UTC));
    }

    @Override
    public void set(PreparedStatement st, Object value, int index) throws SQLException {
        Timestamp ts;
        if(value instanceof Timestamp) {
            ts = (Timestamp) value;
        } else {
            ts = new Timestamp(((java.util.Date) value).getTime());
        }
        st.setTimestamp(index, ts, Calendar.getInstance(UTC));
    }
}

The same thing should be done to fix TimeType and DateType if you use those types. The downside is you'll have to manually specify that these types are to be used instead of the defaults on every Date field in your POJOs (and also breaks pure JPA compatibility), unless someone knows of a more general override method.

UPDATE: Hibernate 3.6 has changed the types API. In 3.6, I wrote a class UtcTimestampTypeDescriptor to implement this.

public class UtcTimestampTypeDescriptor extends TimestampTypeDescriptor {
    public static final UtcTimestampTypeDescriptor INSTANCE = new UtcTimestampTypeDescriptor();

    private static final TimeZone UTC = TimeZone.getTimeZone("UTC");

    public <X> ValueBinder<X> getBinder(final JavaTypeDescriptor<X> javaTypeDescriptor) {
        return new BasicBinder<X>( javaTypeDescriptor, this ) {
            @Override
            protected void doBind(PreparedStatement st, X value, int index, WrapperOptions options) throws SQLException {
                st.setTimestamp( index, javaTypeDescriptor.unwrap( value, Timestamp.class, options ), Calendar.getInstance(UTC) );
            }
        };
    }

    public <X> ValueExtractor<X> getExtractor(final JavaTypeDescriptor<X> javaTypeDescriptor) {
        return new BasicExtractor<X>( javaTypeDescriptor, this ) {
            @Override
            protected X doExtract(ResultSet rs, String name, WrapperOptions options) throws SQLException {
                return javaTypeDescriptor.wrap( rs.getTimestamp( name, Calendar.getInstance(UTC) ), options );
            }
        };
    }
}

Now when the app starts, if you set TimestampTypeDescriptor.INSTANCE to an instance of UtcTimestampTypeDescriptor, all timestamps will be stored and treated as being in UTC without having to change the annotations on POJOs. [I haven't tested this yet]

Sortie answered 7/8, 2010 at 15:29 Comment(7)
How do you tell Hibernate to use your custom UtcTimestampType?Clubfoot
divestoclimb, with which version of Hibernate is UtcTimestampType compatible?Clubfoot
"ResultSet.getTimestamp and PreparedStatement.setTimestamp both say in their docs that they transform dates to/from the current JVM timezone by default when reading and writing from/to the database." Do you have a reference? I don't see any mention of this in the Java 6 Javadocs for these methods. According to #4124034, how these methods apply the time zone to a given Date or Timestamp is JDBC driver dependent.Clubfoot
I could have sworn I read that about using the JVM timezone last year, but now I can't find it. I might have found it on the docs for a specific JDBC driver and generalized.Sortie
To get the 3.6 version of your example to work i had to create a new type which was basically a wrapper around the TimeStampType then set that type on the field.Uncanonical
@divestoclimb, I have an application with Hibernate 3.6. I tried TimestampTypeDescriptor, how can I apply that descriptor to a single entity field? I tried to map my entity with @Type(type="path.to.UtcTimestampTypeDescriptor") but throws an SQLGrammarException: could not execute queryChecani
On Hibernate 4.3.11.Final, your UtcTimestampTypeDescriptor class has two compilation errors: "The type new BasicExtractor<X>(){} must implement the inherited abstract method BasicExtractor<X>.doExtract(CallableStatement, int, WrapperOptions) UtcTimestampTypeDescriptor.java" and "The type new BasicExtractor<X>(){} must implement the inherited abstract method BasicExtractor<X>.doExtract(CallableStatement, String, WrapperOptions)". Apologies if this comment isn't too readable.Haldi
W
27

With Spring Boot JPA, use the below code in your application.properties file and obviously you can modify timezone to your choice

spring.jpa.properties.hibernate.jdbc.time_zone = UTC

Then in your Entity class file,

@Column
private LocalDateTime created;
Weikert answered 27/3, 2019 at 15:58 Comment(3)
It's work for me with ``` private Date lastUpdatedAt; ```Schaab
I love you so muchCrusade
You should not use LocalDateTime in entities, but rather use OffsetDateTime (=defaults to UTC). LocalDateTime is using your local timezone settings, and it also will give you "wrong" datetime when you use datetimes before 1899.Alga
Q
11

Adding an answer that's completely based on and indebted to divestoclimb with a hint from Shaun Stone. Just wanted to spell it out in detail since it's a common problem and the solution is a bit confusing.

This is using Hibernate 4.1.4.Final, though I suspect anything after 3.6 will work.

First, create divestoclimb's UtcTimestampTypeDescriptor

public class UtcTimestampTypeDescriptor extends TimestampTypeDescriptor {
    public static final UtcTimestampTypeDescriptor INSTANCE = new UtcTimestampTypeDescriptor();

    private static final TimeZone UTC = TimeZone.getTimeZone("UTC");

    public <X> ValueBinder<X> getBinder(final JavaTypeDescriptor<X> javaTypeDescriptor) {
        return new BasicBinder<X>( javaTypeDescriptor, this ) {
            @Override
            protected void doBind(PreparedStatement st, X value, int index, WrapperOptions options) throws SQLException {
                st.setTimestamp( index, javaTypeDescriptor.unwrap( value, Timestamp.class, options ), Calendar.getInstance(UTC) );
            }
        };
    }

    public <X> ValueExtractor<X> getExtractor(final JavaTypeDescriptor<X> javaTypeDescriptor) {
        return new BasicExtractor<X>( javaTypeDescriptor, this ) {
            @Override
            protected X doExtract(ResultSet rs, String name, WrapperOptions options) throws SQLException {
                return javaTypeDescriptor.wrap( rs.getTimestamp( name, Calendar.getInstance(UTC) ), options );
            }
        };
    }
}

Then create UtcTimestampType, which uses UtcTimestampTypeDescriptor instead of TimestampTypeDescriptor as the SqlTypeDescriptor in the super constructor call but otherwise delegates everything to TimestampType:

public class UtcTimestampType
        extends AbstractSingleColumnStandardBasicType<Date>
        implements VersionType<Date>, LiteralType<Date> {
    public static final UtcTimestampType INSTANCE = new UtcTimestampType();

    public UtcTimestampType() {
        super( UtcTimestampTypeDescriptor.INSTANCE, JdbcTimestampTypeDescriptor.INSTANCE );
    }

    public String getName() {
        return TimestampType.INSTANCE.getName();
    }

    @Override
    public String[] getRegistrationKeys() {
        return TimestampType.INSTANCE.getRegistrationKeys();
    }

    public Date next(Date current, SessionImplementor session) {
        return TimestampType.INSTANCE.next(current, session);
    }

    public Date seed(SessionImplementor session) {
        return TimestampType.INSTANCE.seed(session);
    }

    public Comparator<Date> getComparator() {
        return TimestampType.INSTANCE.getComparator();        
    }

    public String objectToSQLString(Date value, Dialect dialect) throws Exception {
        return TimestampType.INSTANCE.objectToSQLString(value, dialect);
    }

    public Date fromStringValue(String xml) throws HibernateException {
        return TimestampType.INSTANCE.fromStringValue(xml);
    }
}

Finally, when you initialize your Hibernate configuration, register UtcTimestampType as a type override:

configuration.registerTypeOverride(new UtcTimestampType());

Now timestamps shouldn't be concerned with the JVM's time zone on their way to and from the database. HTH.

Quantifier answered 6/6, 2013 at 6:28 Comment(8)
Would be great to see the solution for JPA and Spring Configuration.Oscillation
A note regarding using this approach with native queries within Hibernate. To have these overridden types be used you must set the value with query.setParameter(int pos, Object value), not query.setParameter(int pos, Date value, TemporalType temporalType). If you use the latter then Hibernate will use its original type implementations, as they are hard coded.Ebbarta
Where should I call the statement configuration.registerTypeOverride(new UtcTimestampType()); ?Bloom
@Bloom Wherever you initialize your Hibernate configuration. If you have a HibernateUtil (most do), it'll be in there.Quantifier
any chance to apply this on hibernate integrated with spring data jpa?Tugboat
It works, but after checking it I realized that it's not required for postgres server working in timezone="UTC" and with all default types of timestamps as "timestamp with time zone" (it's done automatically then). But, here is fixed version for Hibernate 4.3.5 GA as complete single class and overridding spring factory bean pastebin.com/tT4ACXn6Kearney
one way i could get this to work is by adding @Type(type="xx.xx.UtcTimestampType") on the Date field. I am not sure how to register this new type with configuration.Hybridize
Also, you could use @TypeDef to avoid hard coding of class name. If you think this type is used across multiple entity classes in the same package, then create package-info.java like discussed here: vard-lokkur.blogspot.com/2013/04/…Hybridize
E
10

You would think this common problem would be taken care of by Hibernate. But its not! There are a few "hacks" to get it right.

The one I use is to store the Date as a Long in the database. So I am always working with milliseconds after 1/1/70. I then have getters and setters on my Class that return/accept only Dates. So the API remains the same. The down side is that I have longs in the database. SO with SQL I can pretty much only do <,>,= comparisons -- not fancy date operators.

Another approach is to user a custom mapping type as described here: http://www.hibernate.org/100.html

I think the correct way to deal with this is to use a Calendar instead of a Date though. With the Calendar you can set the TimeZone before persisting.

NOTE: Silly stackoverflow won't let me comment, so here is a response to david a.

If you create this object in Chicago:

new Date(0);

Hibernate persists it as "12/31/1969 18:00:00". Dates should be devoid of timezone, so I'm not sure why the adjustment would be made.

Extrasensory answered 17/2, 2009 at 17:33 Comment(6)
Shame on me! You were right and the link from your post explains it well. Now I guess my answer deserves some negative reputation :)Afterguard
Not at all. you encouraged me to post a very explicit example of why this is a problem.Extrasensory
I was able to persist times correctly using the Calendar object so that they are stored in the DB as UTC as you suggested. However, when reading persisted entities back from the database, Hibernate assumes they're in the local time zone and the Calendar object is incorrect!Waugh
John K, in order to resolve this Calendar read problem, I think Hibernate or JPA should provide some way to specify, for each mapping, the time zone to which Hibernate should translate the date that it reads and writes to a TIMESTAMP column.Clubfoot
joekutner, after reading #4124034, I've come to share your opinion that we should store milliseconds since the Epoch in the database rather than a Timestamp since we can't necessarily trust the JDBC driver to store dates as we would expect.Clubfoot
@John K - Could you explain how you could store date in UTC on db? Did you set your JVM to startup on UTC? This is the unique solution that I have found.Illfated
T
8

There are several timezones in operation here:

  1. Java's Date classes (util and sql), which have implicit timezones of UTC
  2. The timezone your JVM is running in, and
  3. the default timezone of your database server.

All of these can be different. Hibernate/JPA has a severe design deficiency in that a user cannot easily ensure that timezone information is preserved in the database server (which allows reconstruction of correct times and dates in the JVM).

Without the ability to (easily) store timezone using JPA/Hibernate then information is lost and once information is lost it becomes expensive to construct it (if at all possible).

I would argue that it is better to always store timezone information (should be the default) and users should then have the optional ability to optimize the timezone away (although it only really affects display, there is still an implicit timezone in any date).

Sorry, this post doesn't provide a work-around (that's been answered elsewhere) but it is a rationalization of why always storing timezone information around is important. Unfortunately it seems many Computer Scientists and programming practitioners argue against the need for timezones simply because they don't appreciate the "loss of information" perspective and how that makes things like internationalization very difficult - which is very important these days with web sites accessible by clients and people in your organization as they move around the world.

Typography answered 13/12, 2010 at 21:7 Comment(2)
"Hibernate/JPA has a severe design deficiency" I'd say it is a deficiency in SQL, which traditionally has allowed the time-zone to be implicit, and therefore potentially anything. Silly SQL.Grit
Actually, instead of always storing the timezone, you can also standardize on one timezone (usually UTC), and convert everything to this timezone when persisting (and back when reading). This is what we usually do. However, JDBC does not support that directly either :-/.Curmudgeon
R
3

Please take a look at my project on Sourceforge which has user types for standard SQL Date and Time types as well as JSR 310 and Joda Time. All of the types try to address the offsetting issue. See http://sourceforge.net/projects/usertype/

EDIT: In response to Derek Mahar's question attached to this comment:

"Chris, do your user types work with Hibernate 3 or greater? – Derek Mahar Nov 7 '10 at 12:30"

Yes these types support Hibernate 3.x versions including Hibernate 3.6.

Runin answered 1/5, 2010 at 15:35 Comment(0)
T
2

Date is not in any time zone (it is a millisecond office from a defined moment in time same for everyone), but underlying (R)DBs generally store timestamps in political format (year, month, day, hour, minute, second, ...) that is time-zone sensitive.

To be serious, Hibernate MUST be allow being told within some form of mapping that the DB date is in such-and-such timezone so that when it loads or stores it it does not assume its own...

Turnspit answered 23/3, 2009 at 19:2 Comment(0)
T
1

Hibernate does not allow for specifying time zones by annotation or any other means. If you use Calendar instead of date, you can implement a workaround using HIbernate property AccessType and implementing the mapping yourself. The more advanced solution is to implement a custom UserType to map your Date or Calendar. Both solutions are explained in my blog post here: http://www.joobik.com/2010/11/mapping-dates-and-time-zones-with.html

Triumvirate answered 8/12, 2010 at 19:26 Comment(0)
D
1

I encountered just the same problem when I wanted to store the dates in the DB as UTC and avoid using varchar and explicit String <-> java.util.Date conversions, or setting my whole Java app in the UTC time zone (because this could lead to another unexpected issues, if the JVM is shared across many applications).

So, there is an open source project DbAssist, which allows you to easily fix the read/write as UTC date from the database. Since you are using JPA Annotations to map the fields in the entity, all you have to do is to include the following dependency to your Maven pom file:

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

Then you apply the fix (for Hibernate + Spring Boot example) by adding @EnableAutoConfiguration annotation before the Spring application class. For other setups installation instructions and more use examples, just refer to the project's github.

The good thing is that you don't have to modify the entities at all; you can leave their java.util.Date fields as they are.

5.2.2 has to correspond to the Hibernate version you are using. I am not sure, which version you are using in your project, but the full list of provided fixes is available on the wiki page of the project's github. The reason why the fix is different for various Hibernate versions is because Hibernate creators changed the API a couple of times between the releases.

Internally, the fix uses hints from divestoclimb, Shane and a few other sources in order to create a custom UtcDateType. Then it maps the standard java.util.Date with the custom UtcDateType which handles all the necessary time zone handling. The mapping of the types is achieved using @Typedef annotation in the provided package-info.java file.

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

You can find an article here which explains why such a time shift occurs at all and what are the approaches to solve it.

Dentoid answered 8/11, 2016 at 13:31 Comment(0)
A
1

Here is the complete checklist for storing dates in your database in a proper way:

  • first, make sure your OS timezone is st properly: either set TZ env variable in your Dockerfile or start your Java application with -Duser.timezone=“UTC”.
  • As mentioned earlier: also set the timezone for JPA with hibernate.jdbc.time_zone = UTC
  • Use OffsetDateTime for storing dates as UTC in your entities. Don’t use Date, Calendar & ZonedDateTime !
Alga answered 5/2, 2023 at 17:31 Comment(1)
Still better than setting user.timezone is writing your code in a way that is independent of the JVM’s default time zone.Magnusson
M
1

It is now 2023. What you want to store from a Java perspecitive is an Instant, right?

So just do that:

public class Event {
    @Id
    public int id;

    @Column("event_timestamp")
    public Instant eventTimestamp;
}

This will work with Hibernate 5.2 or later. You can find many tales on the Internet of having to set hibernate.jdbc.time_zone system property to UTC. This is not necessary with the above, at least not with Hibernate 6. The reason is that Hibernate can see your intent (you specify Instant, not any of the other JSR-310 types), so it knows it has to use a static UTC_CAL when reading or writing the values from/to the database. It will work regardless of your JVM timezone or the timezone of the database server.

The world is luckily much simpler (now) than the other answers on this page leads you to believe.

Mun answered 20/9, 2023 at 13:6 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.