2

I'm working on a Spring Boot REST API that uses MySQL database. I'm trying to save a date (with time) received from client app "as is" which is in UTC time. Basically, it's already agreed upon that the dates will be sent back and forth in UTC and the conversion to appropriate timezone would be done on the client, so I already have UTC time in JSON and I'm trying to save it to a DATETIME column in MySQL.

I chose DATETIME because from MySQL reference it says that TIMESTAMP converts values, while DATETIME doesn't, which is exactly what I need.

I've created all the required entities to map my tables and again, from reference, I've seen that DATETIME maps to java.sql.Timestamp so that's what I used as type.

I'm using JpaRepository and calling it's save method to save my entity. What happens is: when I save dates to MySQL they get converted to UTC (I assume, because I'm in UTC+1 and the dates are one hour earlier than what I inserted).

I've read several answers on SO and I've tried using the following attributes in my jdbc connection string (as suggested here): noDatetimeStringSync=true, useLegacyDatetimeCode=false, sessionVariables=time_zone='-00:00' but none of them worked.

I don't know if maybe spring does something that's messing with this?

This is part of the JSON I receive in request:

{
 "dateFrom": "2019-12-01 16:00:00",
 "dateTo": "2019-12-01 16:30:00"
}

My entity has these properties:

import java.sql.Timestamp;
...
private Timestamp dateFrom;
private Timestamp dateTo;

Table columns in db:

date_from   datetime
date_to     datetime

The entity has the right values inside it when I convert String to Timestamp and invoke save() method. So something between JpaRepository and the db itself messes with the dates. UPDATE: The Timestamp actually wasn't the right value, it added ZoneInfo with ID="Europe/Prague" and zoneOffset=3600000 while converting from String.

What I end up with is 2019-12-01 15:00:00 and 2019-12-01 15:30:00 in db.

What I would like is to store these dates exactly the way I received them. I even thought about switching to VARCHAR because I got so frustrated, but I don't want to do that because I need to perform queries based on dates etc.

Is there any way I can achieve what I'm looking for? It seemed pretty straight-forward at first, but now it's literally driving me insane.

[Additional info if required] I'm using:

  • MySQL 5.7.27
  • Spring Boot (starter-parent) 2.2.0.RELEASE
  • mysql-connector-java

EDIT:

I might be going about this the wrong way, so if you could suggest how I could do this differently that would be awesome. The point is: my app needs to work for users in different timezones and they need to communicate with each other through dates. So if one user in timezone Europe/Prague says "let's talk tomorrow at 5pm" to another user in America/Chicago, I need to store this information in a way that can be translated for user in America in their local time (but also for any other user in any other timezone). This is why I opted for storing dates in UTC and then converting them on the client side to the user's local time. But apparently I'm misunderstanding something about how it all works.

basarito
  • 161
  • 1
  • 13
  • 4
    The date time `String`s do not contain any information about time zone or offset and will be interpreted via system locale or default locale of the database. Check the configurations. – deHaar Nov 08 '19 at 18:52
  • 1
    Adding Timezone information removes the ambiguity. – Subir Kumar Sao Nov 08 '19 at 18:55
  • 1
    what is stored in the `Timestamp` variables? In which timezone? – user85421 Nov 08 '19 at 18:59
  • @deHaar oh okay, that makes sense, I thought it would just store it "as is" without including any timezone. Can I "force" it somehow to UTC? I just tried changing column type to TIMESTAMP, same thing happens. – basarito Nov 08 '19 at 19:06
  • @SubirKumarSao right, but I wanted to remove that part where the server needs to think about timezones? – basarito Nov 08 '19 at 19:07
  • @basarito Server will always store it with a timezone. If you don't provide any it will infer it from local OS settings or configs. Its best to provide the desire timezone. UTC in your case. – Subir Kumar Sao Nov 08 '19 at 19:11
  • @CarlosHeuberger I've just looked into more thoroughly. Even though the preview said `2019-12-01 16:00:00.0`, the cdate inside is actually `2019-12-01T16:00:00.000+0100` with the zoneinfo set to `Europe/Prague` and `zoneOffset=3600000`. This must be the reason why this is happening, but how can I change that? (Btw, I'm not even in Prague, so I'm not sure how it inferred this timezone) – basarito Nov 08 '19 at 19:19
  • try with this: spring.datasource.url=jdbc:mysql://host:port/db-name?useSSL=false&serverTimezone=Europe/Prague&useLegacyDatetimeCode=false – GolamMazid Sajib Nov 08 '19 at 19:55
  • Have you tried the MySQL attributes `useTimezone=true` and `serverTimezone=UTC`, too, as suggested [here](https://stackoverflow.com/questions/14070572/is-java-sql-timestamp-timezone-specific)? – deHaar Nov 08 '19 at 21:19
  • The `Timestamp` class is troublesome. Wouldn’t your JPA implementation allow you to save an `Instant` or an `OffsetDateTime` with offset 0 (UTC)? They would be unambiguous so should prevent any unwanted conversion. – Ole V.V. Nov 08 '19 at 21:34
  • JDBC requires, by specification, that a `java.sql.Timestamp` is applied in the default JVM timezone, unless you use the setters to pass a `Calendar` for the time zone information. – Mark Rotteveel Nov 09 '19 at 13:18
  • 1
    @OleV.V. JDBC does not define support for `Instant`, those are non-standard extensions of some drivers. – Mark Rotteveel Nov 09 '19 at 13:21
  • @deHaar I just tried with these settings. What happens is I send `2019-11-09 14:00:00` it gets stored as `2019-11-09 13:00:00` in db. When I retrieve this later, it's also `2019-11-09 13:00:00`. This is looking better except for the part where I sent `14:00` in the request which should already be in UTC, so it gets converted twice (once presumably on the client app and sent in JSON in UTC and then again on server) so it ends up being the wrong time (`13:00`). Is there any other way I can achieve what I'm looking for? – basarito Nov 09 '19 at 16:16
  • @sajib what would be the purpose of setting it to that timezone? I'm asking because right now my server is in that timezone, but what if it changes location? Would I have to change the jdbc url everytime that happens? – basarito Nov 09 '19 at 16:37

3 Answers3

4

Thanks to deHaar and everyone else who contributed in the comments, I finally got it to work the way I intended it, so I'm going to post a summarized answer in case someone else might need it.

The key was using a standardized date string in JSON request/response, along with proper Java 8 DateTime objects. What I was originally sending simply wasn't enough.

So according to ISO-8601 date and time formats:

Times are expressed in UTC (Coordinated Universal Time), with a special UTC designator ("Z").

Note that the "T" appears literally in the string, to indicate the beginning of the time element, as specified in ISO 8601.

Meaning, my API should communicate only in these standardized UTC date strings and no custom date string stuff, e.g. :

{
    "dateFrom": "2019-12-01T16:00:00Z",
    "dateTo": "2019-12-01T16:30:00Z"
}

Another thing I did, as mentioned in the comments, was set the db timezone to UTC and leave nothing to chance. Since I use spring boot there were two ways to do this:

  1. In application.properties set property: spring.jpa.properties.hibernate.jdbc.time_zone=UTC

  2. Or in the jdbc connection string: serverTimezone=UTC

I've also added useLegacyDatetimeCode=false parameter to the string above, because I've read that without it, the serverTimezone param has no effect (someone is free to correct me if I'm wrong about this).

Now, there is some debate whether the java.sql.Timestamp has timezone information or not. I went through SO to read more about it and it turns out that previous versions (before Java 8) indeed didn't have timezone information (like here for example). However, as I clearly saw in the debugger, Timestamp object has zone info stored separately, but it does have it.

According to Mark Rotteveel from the comments

a java.sql.Timestamp by specification represents time in the default JVM timezone

which makes sense, cause when I changed JVM timezone, the Timestamp values changed as well.

Now, to tackle this issue, I've seen lots of suggestions to set the default timezone to UTC (cause if it didn't have it set, it would naturally fallback to JVM's). I tried this as well by using the following code

System.setProperty("user.timezone", "UTC");

and it worked because it was now converting the db values to UTC, but what I didn't like about this approach is that it changed everything to UTC time (duh), including my logs. It just didn't feel natural to do it "forcefully" like this.

Another discovery that was very meaningful to me is that everyone kept saying to switch completely to java.time and its classes, which made like 80% of answers on SO pretty deprecated and useless, since they suggest using old classes to do stuff like parsing, conversion etc. (Still not sure if I'm able to completely ditch java.sql.Timestamp as well, since for now the docs say that this is the type to map to from datetime format in DB, but for now I'll leave it).

Finally, what I did was create a little util class to help me with the conversions I need.

import java.sql.Timestamp;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;

public class DateTimeUtil {

    public static Timestamp getTimestamp(String utcDateTime) {
        LocalDateTime localDateTime = LocalDateTime.parse(utcDateTime, DateTimeFormatter.ISO_DATE_TIME);
        return Timestamp.from(localDateTime.atZone(ZoneId.of("UTC")).toInstant());
    }

    public static String getUTCString(Timestamp dbTimestamp) {
        return dbTimestamp.toInstant().atZone(ZoneId.of("UTC")).toString();
    }

}

The first function is used for parsing string date from JSON request.

I opted for LocalDateTime instead of ZonedDateTime because of this info from java.time doc:

Where possible, it is recommended to use a simpler class without a time-zone. The widespread use of time-zones tends to add considerable complexity to an application.

(EDIT: The code was updated, using LocalDateTime was incorrect exactly because it strips timezone of UTC and then behaves exactly like JVM situation did. Sorry, I tested this wrongly)

The second function is used for sending response in JSON. While the values were being stored the way they should in DB, when I retrieved them back, they got converted to my local timezone (since JVM is in a different timezone). That's why it needed to be said "hey, this value is UTC timezone, show it like that, not in my local timezone".

So now, before saving my entity to DB, I set its timestamp with getTimestamp() which saves it as UTC, and when I retrieve the value and prepare my DTO fro response, I use getUTCString() which also makes it ISO formatted UTC, all the while the DB acts like it's located in UTC so it can't add something of its own to the mix AND the communication between the client and the API is standardized and everyone knows exactly what to expect.

Community
  • 1
  • 1
basarito
  • 161
  • 1
  • 13
1

If you want to make sure you are dealing with UTC times, then you should store them as such in the database. You can specifiy a zone or an offset in Java:

public ZonedDateTime utcDateTimeFrom(Timestamp timestamp) {
    return timestamp.toLocalDateTime().atZone(ZoneId.of("UTC"));
}

public Timestamp toUtcTimestamp(LocalDateTime localDateTime) {
    return Timestamp.from(localDateTime.atZone(ZoneId.of("UTC")).toInstant());
}

Try the methods or write similar ones (there is OffsetDateTime, too), but I think the date time in your JSON response/request is not sufficient, you will have to send the time zone or the offset from client side, too, otherwise you may get into trouble with the time zones.

deHaar
  • 17,687
  • 10
  • 38
  • 51
  • You're right, my JSON wasn't good. After more research I decided to go with ISO-8601 standard and add the "Zulu time" to my string. So now when I send e.g. `2019-12-01T12:00:00Z` it gets stored like it should. I still had issues with retrieving the value, cause the JDBC or Timestamp kept converting it back to my local time, but I did a mix of all your suggestions and finally got it to work the way it should. I'm going to post it as an answer just cause it covers everything in my question, but I couldn't have done it without all of your suggestions. So thank you! – basarito Nov 12 '19 at 12:42
  • @basarito You're welcome... I'm looking forward to your solution ;-) – deHaar Nov 12 '19 at 12:52
0

Application's timezone is used while converting Database's DateTime to java.sql.Timestamp. You would need to pass environment variables or set it in application.

-Duser.timezone=America/Chicago

or

TimeZone.setDefault(TimeZone.getTimeZone("America/Chicago"));

Edit:

I think you are missing a very important point.

java.sql.Timestamp is nothing but a long value which is the number of milliseconds elapsed since 1971. This is independent of time zones, they are needed only when you want to get string representation.

When you create object of java.sql.Timestamp from some string representation 2019-12-01 16:00:00 there itself your value would have got shifted by using your server timezone.

Coming to your second question.

For example - 1573251160 is the time when i am writing this answer. This value will mean same Instant in time in all timezones.

Now this needs to be converted to DateTime Column to store in database which is a timezone dependent field. Without knowing the timezone you simply cannot choose a time as time will be different in different timezones. Without knowing the timezone its impossible to convert. For example : Same value will get converted to:

  1. Fri, 08 Nov 2019 22:12:40 GMT
  2. Fri, 09 Nov 2019 3:42:40 AM GMT+05:30

The conversion will depend on which timezone your server runs on. If you are always expecting your values to get stored in UTC it seems reasonable to run your server on UTC.

akash300
  • 49
  • 1
  • 8
  • Is it possible not to use any conversion but read it like it is, since I've already received a UTC time (meaning I know that my dates are stored as UTC and will add the timezone on the client side when it should be presented to the user in _their_ timezone)? I feel like setting the timezone like this would make the conversion to user's local time difficult. – basarito Nov 08 '19 at 19:33
  • 1
    _"java.sql.Timestamp is nothing but a long value which is the number of milliseconds elapsed since 1971."_ although that is the **implementation**, a `java.sql.Timestamp` by specification represents time in the default JVM timezone. – Mark Rotteveel Nov 09 '19 at 13:19