I came upon a problem with java.time classes and their mapping to DB types. I want to store Instant type, but it behaves quite unintuitively.
Simple project example here: https://gitlab.com/Gobanit/jpa-time-example
I have basic entity class:
#Entity
#Getter
#Setter
public class AbstractEntity {
#Id
#GeneratedValue
private Long id;
// #Column(columnDefinition = "TIMESTAMP WITH TIME ZONE")
private Instant createdAt;
// #Column(columnDefinition = "TIMESTAMP WITH TIME ZONE")
private ZonedDateTime createdAtZoned;
private LocalDateTime createdAtLocal;
private Instant modifiedAt;
#PrePersist
protected void prePersist() {
createdAt = Instant.now();
modifiedAt = createdAt;
createdAtZoned = createdAt.atZone(ZoneId.systemDefault());
createdAtLocal = createdAtZoned.toLocalDateTime();
System.out.println(String.format("Created: instant=%s, zoned=%s, local=%s", createdAt, createdAtZoned, createdAtLocal));
}
#PreUpdate
protected void preUpdate() {
modifiedAt = Instant.now();
}
}
Now, I will create and persist the entity:
#Transactional
#GetMapping("create")
public AbstractEntity createNew() {
System.out.println("create");
System.out.println("ZoneId=" + ZoneId.systemDefault());
var e = new AbstractEntity();
em.persist(e);
return e;
}
The data in Java are as expected - system output below:
ZoneId=Europe/Bratislava
Created: instant=2021-12-29T14:13:55.624902400Z, zoned=2021-12-29T15:13:55.624902400+01:00[Europe/Bratislava], local=2021-12-29T15:13:55.624902400
However, the data in DB are not. They are all stored as the same value. The default type of the column is set to TIMESTAMP for all of them. And they are all stored as if they were LocalDateTime in application JVM timezone.
I dont like this at all, there is no difference between these types and the "meaning" of the value is dependent on the application zone. If I look at the database without knowing which application wrote the data and what is its zone, i cannot say, what the datetime means. Even worse, if I run the application with different zone, it would assume that the values were written in its timezone, which they were not.
Some people say, the time data should be stored in UTC (which i partially agree) and they recommend setting following property, so that the values are converted to UTC zone before storing in DB by JDBC:
spring.jpa.properties.hibernate.jdbc.time_zone: UTC
However, this results in even worse behavior, since it applies not only to Instant/ZonedDateTime but also to LocalDateTime, which seems logically wrong to me. LocalDateTime value should not be affected by any timezone conversions at all, that is why it is LocalDateTime.
System Output:
ZoneId=Europe/Bratislava
Created: instant=2021-12-29T14:15:13.614799900Z, zoned=2021-12-29T15:15:13.614799900+01:00[Europe/Bratislava], local=2021-12-29T15:15:13.614799900
DB Result:
I would expect one of these to happen:
Mapping Instant and ZonedDateTime to TIMESTAMP_WITH_TIMEZONE by default. Also, Instant type should be always serialized with UTC zone - just as when printing to string or serializing with jackson. (ideal)
Mapping Instant and ZonedDateTime to TIMESTAMP but convert them to UTC when serializing, and back to JVM timezone when deserializing. (not ideal, but good enough)
I can override the default mapping explicitly, by uncommenting the line below.
// #Column(columnDefinition = "TIMESTAMP WITH TIME ZONE")
That works fine, only Instant are not converted to UTC but take the JVM zone, just like ZonedDateTime. Technically it is totally fine, i cant see any inconsistency result happen because of that, but personally i would prefer them in UTC, so that they are always unified.
The second problem with this, is that i need to explicitly configure it for each field.
ZoneId=Europe/Bratislava
Created: instant=2021-12-29T14:19:14.600583400Z, zoned=2021-12-29T15:19:14.600583400+01:00[Europe/Bratislava], local=2021-12-29T15:19:14.600583400
In the end, I have few questions, I would really like someone to answer me :)
**
Is there any way to get the ideal solution (the first mentioned)?
Is there any way to at least globally configure the type mapping?
Can you see any risks in this approach?
Is there a better or more standardized approach, that I am not aware of? What is your strategy for working with time?
Thanks!
I cannot speak to JPA, but I can explain some of the JDBC issues.
I want to store Instant type
The JDBC specification does not map Instant.
For a column of a type akin to the SQL standard type TIMESTAMP WITHOUT TIME ZONE, use the Java class LocalDateTime.
For a column of a type akin to the SQL standard type TIMESTAMP WITH TIME ZONE, use the Java class OffsetDateTime.
In JDBC, use setObject & getObject to exchange data-time values as java.time values in JDBC 4.2 and later.
myPreparedStatement.setObject( … , myOffsetDateTime ) ;
Some databases such as Postgres automatically adjust the incoming data to UTC, an offset of zero hours-minutes-seconds. For portable code across databases, you may want to make that adjustment in Java code.
OffsetDateTime myOdt = otherOdt.withOffsetSameInstant( ZoneOffset.UTC ) ;
Capture the current moment as a OffsetDateTime.
OffsetDateTime myOdt = OffsetDateTime.now( ZoneOffset.UTC ) ;
If you have an Instant in hand, convert to OffsetDateTime for storage in a database via JDBC.
OffsetDateTime myOdt = myInstant.atOffset( ZoneOffset.UTC ) ;
There is no need for you to store the zoned or unzoned values in your database. You can easily adjust to time zones after database retrieval, in your Java code, as you would do for other localization needs.
ZoneId z = ZoneId.of( "America/Edmonton" ) ;
ZonedDateTime zdt = myOdt.atZoneSameInstant( z ) ;
I see no benefit generally to using LocalDateTime when dealing with moments, specific points on the timeline. That class purposely lacks the context of a time zone or offset. Such objects contain a date with time of day only, so are inherently ambiguous. You can convert from OffsetDateTime to LocalDateTime but would be stripping away vital information while gaining nothing in return.
they are all stored as if they were LocalDateTime in application JVM timezone.
That is a contradiction in terms. A LocalDateTime has no time zone or offset.
The default type of the column is set to TIMESTAMP for all of them.
There is no default for the data type of a column. You specify the data type when creating the column. The data type does not change while inserting rows. The data type can only change if you issue an ALTER TABLE command.
Also, we have no idea what type you mean with the word TIMESTAMP since you neglected to mention your specific database. As mentioned above, the SQL standard uses four words for each of the two very different timestamp types.
Mapping Instant and ZonedDateTime to TIMESTAMP but convert them to UTC when serializing,
No, JDBC does not require any such mapping of Instant nor ZonedDateTime.
You need to do such conversions in your Java code, outside of the JDBC calls.
The exception is that your particular JDBC driver may choose to go beyond what is required by the JDBC spec. A driver is free to handle the Instant and ZonedDateTime types if it’s creators saw fit. But beware that any such code you wrote may not be portable across other JDBC drivers.
and back to JVM timezone when deserializing.
I believe you’ll find this work much easier if, as a programmer/DBA/SysAdmin, you do most of your thinking, logging, debugging, data storage, and data exchange, in UTC (offset of zero).
Converting to Instant is a simple way of adjusting to UTC.
Instant instant = odt.toInstant() ;
Instant instant = zdt.toInstant() ;
Tip: Generally write your code to not depend on the default time zone of your JVM, your database session, or host OS. Those defaults are not under your control as a programmer. And those defaults can change at any moment during runtime. So use a default only where called for, such as localizing to default zone of a user’s mobile device for presentation in the user-interface. And make your use of such a default explicit, always specifying the otherwise optional zone/offset arguments to various calls.
Tip: Recording the moment a row is inserted or updated is generally best left to the database. Use a trigger where the database server captures the current moment and assigns it to the field. Then you are covered for operations outside your Java app, such as bulk data loads.
Related
I am using hibernate + spring and want to store/load timestamps in UTC. I've read that I should add a property, so I added this to my application.properties
spring.jpa.properties[hibernate.jdbc.time_zone]=UTC
This worked for one part of the problem - now dates are saved in utc in the database. But when I retrieve timestamps, they are transformed into default timezone. How can I fix this without setting default time zone to UTC?
The property of the entity has type LocalDateTime.
I ran the code, and noticed that the proper result set method is used during get(the one that accepts calendar) with instance that has zone info storing UTC. But after setting calendar's values to the one retrieved from the database, the calendar is transformed into Timestamp with this code
Timestamp ts = new Timestamp(c.getTimeInMillis());
In debug mode, I see that ts stores cdate field with value of timestamp in default time zone(not UTC).
First of all, if we are talking about Hibernate 5 (5.2.3 - 5.6.x if to be precise) the purpose of hibernate.jdbc.time_zone setting is not to give the ability for application developer to implement some kind of sophisticated date/time logic, but to synchronize persistence provider with underlying database, that is clearly stated in the corresponding CR:
Currently my database has implicit date times in UTC. No zoned data is appended to the end of the string (e.g. "2013-10-14 04:00:00").
When Hibernate reads this as a ZonedDateTime, it incorrectly reads it in as EST, as that is the TimeZone of the JVM.
It would be nice to be able to specify the TimeZone of a field by an annotation perhaps.
basically: you definitely need to set up hibernate.jdbc.time_zone if (mine: and only if) SQL statement like SELECT SYSDATE FROM DUAL (SELECT LOCALTIMESTAMP for PostgreSQL, etc) returns something, what you do not expect, in that case Hibernate will start adjusting non-timezone-aware JDBC data to something more or less reliable for application - that is exactly what you are observing (when I retrieve timestamps, they are transformed into default timezone)
At second, any speculations around JSR-310 and JDBC 4.2 (like for timezone-aware java types you need to define DB columns as timestamp with time zone), are not correct in case of Hibernate 5, that is mentioned in the corresponding CR as well:
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 ...
And indeed, if you try to find usage of java.sql.Types#TIMESTAMP_WITH_TIMEZONE in Hibernate 5 sources you will find nothing, just because that time Hibernate developers didn't get a common opinion about how timezone conversions should work in cases of different Java versions, Java types, DB engines and JDBC drivers (they are developing the most popular (mine: the only one) JPA implementation, that is definitely not the same as develop microservice), however, there are a lot of related changes in Hibernate 6 (check TimeZoneStorageType for example). In Hibernate 5 all timezone conversion logic passes through TimestampTypeDescriptor:
#Override
protected X doExtract(ResultSet rs, String name, WrapperOptions options) throws SQLException {
return options.getJdbcTimeZone() != null ?
javaTypeDescriptor.wrap( rs.getTimestamp( name, Calendar.getInstance( options.getJdbcTimeZone() ) ), options ) :
javaTypeDescriptor.wrap( rs.getTimestamp( name ), options );
}
and as you can see, Hibernate 5 just gives a hint to JDBC driver, how the last should process #getTimestamp call:
Retrieves the value of a JDBC TIMESTAMP parameter as a java.sql.Timestamp object, using the given Calendar object to construct the Timestamp object. With a Calendar object, the driver can calculate the timestamp taking into account a custom timezone and locale. If no Calendar object is specified, the driver uses the default timezone and locale.
in regard to your case:
you either need to use timezone-aware java types (ZonedDateTime/OffsetDateTime, or even Instant) or code your own Hibernate type, which will handle timezone conversions - that is not so hard as it might seem.
we can also set it up` per-session basis:
session = HibernateUtil.getSessionFactory().withOptions()
.jdbcTimeZone(TimeZone.getTimeZone("UTC"))
.openSession();
My database timezone was in UTC and in my application timezone I solved this problem by having both the Entity and the table have a date in UTC so that there will need to be no conversion between them. Then I did the conversions between timestamps in code in the getters and setters. Then I think you do it manually.
Setter and getter for that field:
public void setCreatedDate(LocalDateTime createdAt)
{
this.createdAt = createdAt.atZone(ZoneId.systemDefault()).withZoneSameInstant(ZoneOffset.UTC).toLocalDateTime();
}
public LocalDateTime getCreatedDate()
{
return createdAt.atZone(ZoneId.systemDefault()).withZoneSameInstant(ZoneOffset.UTC).toLocalDateTime();
}
As alluded to be Andrey in his answer, in Hibernate 6 the way to normalize dates/times to UTC is to use the java.time types OffsetDateTime or ZonedDateTime, and either:
annotate the field #TimeZoneStorage(NORMALIZE_UTC), or
set the property hibernate.timezone.default_storage=NORMALIZE_UTC.
I'm not very certain what you mean about using LocalDateTime here. What would it even mean to normalize a local datetime to UTC? That statement just doesn't really make sense: you can't move a local datetime to a new time zone because it doesn't have an associated time zone to begin with.
I think what you mean is that your "local" date times are actually zoned datetimes in the current JVM time zone. But if that's the case, it's very easy to use localDateTime.atZone(ZoneId.systemDefault()) to represent that situation correctly.
Part 1:
I have a file which contains date as string in the following format: 2006-02-16T21:36:32.000+0000
I need to write this value to Postgres in a column which is of type Timestamp. How do I go about doing this in Java?
Part 2:
I need to read the value saved in Part 1 from Postgres, and convert it to a string of the following format "2006-02-16T21:36:32.000+0000"
This is what I have tried:
Timestamp lastSyncDate = Timestamp.valueOf(2006-02-16T21:36:32.000+0000)
When I try to write it to postgres, it gives me the following error:
java.lang.IllegalArgumentException: Timestamp format must be yyyy-mm-dd hh:mm:ss[.fffffffff]
tl;dr
myPreparedStatement.setObject(
… ,
OffsetDateTime.parse( "2006-02-16T21:36:32.000+0000" )
)
With and without zone/offset
You said your column is of type TIMESTAMP which is short for TIMESTAMP WITHOUT TIME ZONE. This type purposely lacks any concept of time zone or offset-from-UTC. Not usually appropriate for common business purposes.
You are using wrong type
That is the wrong type for your input. Your input has an offset-from-UTC of zero hours which means UTC itself. Having an offset or zone, your data should only be stored in a TIMESTAMP WITH TIME ZONE type column. In this …WITH… type in Postgres, any submitted offset/zone info is used to adjust into UTC for storage, and then discarded.
Storing a date-time value with an offset or zone in a column of type TIMESTAMP WITHOUT TIME ZONE is like storing a price/cost with a designated currency( USD, Euros, Rubles, etc.) in a column of numeric type. You are losing vital data, the currency. This renders your data worthless.
See the Postgres documentation page for these types.
Smart objects, not dumb strings
Whenever possible, use objects to exchange data with your database rather than passing meter strings. Let your JDBC driver do it’s job in marshaling the data back-and-forth.
Parse the input string as an OffsetDateTime object.
ISO 8601
Your input strings are in a format defined by the ISO 8601 standard. The java.time classes use ISO 8601 formats by default when parsing/generating strings. No need to specify a formatting pattern.
String input = "2006-02-16T21:36:32.000+0000" ;
OffsetDateTime odt = OffsetDateTime.parse( input ) ;
Define your SQL as a prepared statement.
With JDBC 4.2 and later, you can directly exchange java.time objects with your database. No need to ever again use the troublesome legacy classes such as java.sql.Timestamp.
You could pass an OffsetDateTime. I like to extract an Instant to demonstrate to the reader that I understand how Postgres always stores a TIMESTAMP WITH TIME ZONE value in UTC.
Instant instant = odt.toInstant() ;
myPreparedStatement.setObject( … , instant ) ;
Retrieval.
Instant instant = myResultSet.getObject( … , Instant.class ) ;
2006-02-16T21:36:32.000+0000 looks like a ISO-8601 timestamp.
postgres understands this format. just treat it like any other string.
don't try to convert it into a java timestamp
I have the following code. I have stored a utc time in my mySQL db and would like to convert it to local time when I pull it from the database.
Instant startInstant = rs.getTimestamp(5).toInstant();
Instant endInstant = rs.getTimestamp(6).toInstant();
ZoneId z = ZoneId.of(TimeZone.getDefault().getID());
ZonedDateTime start = startInstant.atZone(z);
ZonedDateTime end = startInstant.atZone(z);
System.out.println(start.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss VV")));
System.out.println(end.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss VV")));
This is the output I get:
2017-11-01 01:40:00 America/Chicago
2017-11-01 01:40:00 America/Chicago
The date and time are the same as in my db: This is the field value in the table:
2017-11-01 01:40:00
The only thing that seems to have updated is just the zoneID to America/Chicago which is functionally useless for what I'm needing.
Any pointers as to where I've gone wrong?
You have not supplied enough information to give an exact Answer. You did not explain exactly the data type of the column around your date-time value. I suspect you did not use the MySQL equivalent to the SQL standard type of TIMESTAMP WITH TIME ZONE.
I also wonder why your example of a stored value lacks any offset-from-UTC or time zone. That is suspicious.
Lastly, you may be looking st your stored value by using an interactive SQL session with a default time zone bring dynamically applied to the value retrieved from the database before displaying in your session. While well-intentioned, I consider this an anti-feature as it masks the true nature of the stored data. This behavior may explain your confusion. Try explicitly setting your session’s default time zone to UTC.
Other issues with your code…
Skip the use of java.sql.Timestamp altogether. If your JDBC driver supports JDBC 4.2 or later, you can work directly with the java.time types.
Instant instant = myResultSet.getObject( … , Instant.class ) ;
Do not mix an old legacy class like TimeZone with the modern java.time classes. The legacy classes are completely supplanted by java.time; good riddance.
ZoneId z = ZoneId.systemDefault() ;
You are getting the same value that you have entered in a database.
You have timestamp values in database, but how you can say that the values stored
are in UTC (the values might be in UTC but not in UTC format, so by looking into the value, you can't say that the value is in UTC -> '2017-11-01 01:40:00').
If you want to fetch the value in the different time zone, you can also do like this :
eg:
SET time_zone ='+03:00';
SELECT value
FROM table;
It will be providing you the value in specified time zone.
I have these two classes
class Source {
// mapped to TIMESTAMP
#Version
#Column(columnDefinition="TIMESTAMP(3) DEFAULT '2016-01-01'")
Instant myInstant;
}
class Destination {
// mapped to DATETIME
#Basic(optional=true)
Instant myInstant;
}
When using Hibernate, I assign
destination.myInstant = source.myInstant;
and then the stored value is smaller by one hour than the original - both according to the command line MySQL client and in Java. My current timezone is UTC+1, so the reason is obviously a timezone conversion.
There are a few places where this can be fixed, but I'm looking for the best practice. The server should work world-wide, so it should continue to use UTC internally, right?
Should I just change the column type to TIMESTAMP? Then, why does Instant by default map to DATETIME?
According to this article, Instant does map to TIMESTAMP, but in my case it did not. Why?
If you want to work with timezones and Java 8 I would recommend using ZonedDateTime or OffsetTimeZone (the latter being prefered when working with Hibernate). For older versions use Calendar.
When you instance it should go by default with the timezone of your computer.
Check if the database is timestamp with or without timezone.
The default you set is also without timezone, and if it is "with timezone" it should automatically add the database's offset.
I hope some of this works. Here's how I did in one of my projects.
#Column(name = "registration_time")
private OffsetDateTime registrationTime;
[...]
subscriber.setRegistrationTime(OffsetDateTime.now());
In MySQL 5 & above, TIMESTAMP values are converted from the current time zone to UTC for storage, and converted back from UTC to the current time zone for retrieval. This occurs only for the TIMESTAMP data type but not for DATETIME. This is the reason you are seeing the difference while assigning a TIMESTAMP to DATETIME. So, having both the columns of same type should work. Hibernate by default maps InstantType to database TIMESTAMP type. Though you could use it for both TIMESTAMP and DATETIME in MYSQL, they are handled differently.
I'm storing two dates in the PostgreSQL database. First, is the data of visit of a webpage, and the second date is the date of last modification of the webpage(this is get as a long).
I have some doubts what is the best strategy to store these values.
I only need day/month/year and hour:seconds and this will only for statistical proposes.
So, some doubts:
is best store as long and convert on recover of information or store in the data format above?
is best set the date of visit on the software or in the insertion in the database?
in Java, how are the best classes to handle dates?
Any strategy for storing date-and-time data in PostgreSQL should, IMO, rely on these two points:
Your solution should never depend on the server or client timezone setting.
Currently, PostgreSQL (as most databases) doesn't have a datatype to store a full date-and-time with timezone. So, you need to decide between an Instant or a LocalDateTime datatype.
My recipe follows.
If you want to record the physical instant at when a particular event ocurred, (a true "timestamp" , typically some creation/modification/deletion event), then use:
Java: Instant (Java 8 , or Jodatime).
JDBC: java.sql.Timestamp
PostgreSQL: TIMESTAMP WITH TIMEZONE (TIMESTAMPTZ)
(Don't let PostgreSQL peculiar datatypes WITH TIMEZONE/WITHOUT TIMEZONE confuse you: none of them actually stores a timezone)
Some boilerplate code: the following assumes that ps is a PreparedStatement, rs a ResultSet and tzUTC is a static Calendar object corresponding to UTC timezone.
public static final Calendar tzUTC = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
Write Instant to database TIMESTAMPTZ:
Instant instant = ...;
Timestamp ts = instant != null ? Timestamp.from(instant) : null;
ps.setTimestamp(col, ts, tzUTC); // column is TIMESTAMPTZ!
Read Instant from database TIMESTAMPTZ:
Timestamp ts = rs.getTimestamp(col,tzUTC); // column is TIMESTAMPTZ
Instant inst = ts !=null ? ts.toInstant() : null;
This works safely if your PG type is TIMESTAMPTZ (In that case, the calendarUTC has no effect in that code ; but it's always advisable to not depend on defaults timezones).
"Safely" means that the result will not depend on server or database timezone, or timezones information: the operation is fully reversible, and whatever happens to timezones settings, you'll always get the same "instant of time" you originally had on the Java side.
If, instead of a timestamp (an instant on the physical timeline), you are dealing with a "civil" local date-time (that is, the set of fields {year-month-day hour:min:sec(:msecs)}), you'd use:
Java: LocalDateTime (Java 8 , or Jodatime).
JDBC: java.sql.Timestamp
PostgreSQL: TIMESTAMP WITHOUT TIMEZONE (TIMESTAMP)
Read LocalDateTime from database TIMESTAMP:
Timestamp ts = rs.getTimestamp(col, tzUTC); //
LocalDateTime localDt = null;
if( ts != null )
localDt = LocalDateTime.ofInstant(Instant.ofEpochMilli(ts.getTime()), ZoneOffset.UTC);
Write LocalDateTime to database TIMESTAMP:
Timestamp ts = null;
if( localDt != null)
ts = new Timestamp(localDt.toInstant(ZoneOffset.UTC).toEpochMilli()), tzUTC);
ps.setTimestamp(colNum,ts, tzUTC);
Again, this strategy is safe and you can sleep peacefully: if you stored 2011-10-30 23:59:30 , you'll retrieve those precise fields (hour=23, minute=59... etc) always, no matter what - even if tomorrow the timezone of your Postgresql server (or client) changes, or your JVM or your OS timezone, or if your country modifies its DST rules, etc.
Added: If you want (it seems a natural requirement) to store the full datetime specification (a ZonedDatetime: the timestamp together with the timezone, which implicitly also includes the full civil datetime info - plus the timezone)... then I have bad news for you: PostgreSQL hasn't a datatype for this (neither other databases, to my knowledge). You must devise your own storage, perhaps in a pair of fields: could be the two above types (highly redundant, though efficient for retrieval and calculation), or one of them plus the time offset (you lose the timezone info, some calculations become difficult, and some impossible), or one of them plus the timezone (as string; some calculations can be extremely costly).
java.time
It's not pretty but this is what worked for me with a ZonedDateTime instance using the new java.time framework in Java 8 and later (Tutorial):
ZonedDateTime receivedTimestamp = some_ts_value;
Timestamp ts = new Timestamp(receivedTimestamp.toInstant().toEpochMilli());
ps.setTimestamp(
1,
ts,
Calendar.getInstance(TimeZone.getTimeZone(receivedTimestamp.getZone()))
);
I had used java.util.Date in our code to store Date in arguments of my query. I was using Spring JPA and using #Query annotations. The implementation worked.