1

I need to be able to convert data fetched from MySQL database that is stored in a "datetime" fields into Java ZonedDateTime object.

ZonedDateTime dt = ZonedDateTime.ofInstant(rs.getTimestamp("Start").toInstant(), UTC_ZONE_ID)

The problem I'm having is that toInstant() adds local time offset to the Timestamp object which I don't need because the datetime is already stored in a UTC format in a database. So when I run the following code:

ZonedDateTime startDT = 
        ZonedDateTime.ofInstant(rs.getTimestamp("Start").toInstant(),Globals.LOCALZONEID);
System.out.println(rs.getTimestamp("start"));
System.out.println(rs.getTimestamp("start").toInstant());

I get:

2017-06-08 13:15:00.0
2017-06-08T17:15:00Z

I need the time component to stay unchanged.

I was unable to find any obvious solution to the problem so am I missing something here?

Ole V.V.
  • 81,772
  • 15
  • 137
  • 161
AnKing
  • 1,994
  • 6
  • 31
  • 54

3 Answers3

18

Timestamp & Instant always in UTC

The problem I'm having is that .toInstant() add local time offset to the Timestamp object

No it does not.

Neither can have any other zone assigned.

Do not bunch up your code into a single line. Break out each step into separate lines so you can debug their values.

java.sql.Timestamp ts = rs.getTimestamp("Start") ;  // Actually in UTC, but it's `toString` method applies JVM’s current default time zone while generating string.
Instant instant = ts.toInstant() ;                  // Same moment, also in UTC.
ZoneId z = ZoneId.of( "America/Montreal" ) ;        // Or call your global var: `Globals.LOCALZONEID`.
ZonedDateTime zdt = instant.atZone( z );            // Same moment, same point on timeline, but with wall-clock time seen in a particular zone.

After that, you may see the issue (or non-issue). If not, edit your question to show the debug values of each of these variables.

Do not trust Timestamp::toString

Important: The java.sql.Timestamp::toString method lies. That method applies your JVM’s current default time zone while generating the string. The actual value is always in UTC. One of many reasons to avoid these troublesome legacy classes. Run the following code example on your own machine to see the influence of your default time zone on the textual representation of the Timestamp.

Let’s run a simulation of that code running live in IdeOne.com. The JVM at IdeOne.com defaults to UTC/GMT, so we override default by specifying the default as Pacific/Auckland arbitrarily.

Instant now = Instant.now() ;                       // Simulating fetching a `Timestamp` from database by using current moment in UTC.

TimeZone.setDefault( TimeZone.getTimeZone( "Pacific/Auckland" ) ) ;
ZoneId zoneIdDefault = ZoneId.systemDefault() ;
ZoneOffset zoneOffset = zoneIdDefault.getRules().getOffset( now ) ;

java.sql.Timestamp ts = java.sql.Timestamp.from( now ) ;  // Actually in UTC, but it's `toString` method applies JVM’s current default time zone while generating string.
Instant instant = ts.toInstant() ;                  // Same moment, also in UTC.
ZoneId z = ZoneId.of( "America/Montreal" ) ;        // Or call your global var: `Globals.LOCALZONEID`.
ZonedDateTime zdt = instant.atZone( z );            // Same moment, same point on timeline, but with wall-clock time seen in a particular zone.

Current default time zone: Pacific/Auckland

Current default offset-from-UTC: Pacific/Auckland | total seconds: 43200

now.toString(): 2017-06-09T04:41:10.750Z

ts.toString(): 2017-06-09 16:41:10.75

instant.toString(): 2017-06-09T04:41:10.750Z

z.toString(): America/Montreal

zdt.toString(): 2017-06-09T00:41:10.750-04:00[America/Montreal]

Avoid legacy date-time classes

The old date-time classes found outside the java.time package are troublesome, confusing, badly designed, and flawed. Avoid them whenever possible. This includes java.sql.Timestamp.

Your JDBC 4.2 compliant driver can directly address java.time types by calling PreparedStatement::setObject and ResultSet::getObject.

myPreparedStatement.setObject( … , instant ) ;

… and …

Instant instant = myResultSet.getObject( … , Instant.class ) ;

If using a JDBC driver not yet updated to JDBC 4.2 and java.time, convert briefly to java.sql.Timestamp using new methods added to the old class: from ( Instant ), toInstant(), and such. But beyond exchanging data with the database, do all your real work (business logic) in java.time objects.

myPreparedStatement.setTimestamp( … , java.sql.Timestamp.from( instant ) ) ;

… and …

Instant instant = myResultSet.getTimestamp( … ).toInstant() ;

About java.time

The java.time framework is built into Java 8 and later. These classes supplant the troublesome old legacy date-time classes such as java.util.Date, Calendar, & SimpleDateFormat.

The Joda-Time project, now in maintenance mode, advises migration to the java.time classes.

To learn more, see the Oracle Tutorial. And search Stack Overflow for many examples and explanations. Specification is JSR 310.

You may exchange java.time objects directly with your database. Use a JDBC driver compliant with JDBC 4.2 or later. No need for strings, no need for java.sql.* classes.

Where to obtain the java.time classes?

The ThreeTen-Extra project extends java.time with additional classes. This project is a proving ground for possible future additions to java.time. You may find some useful classes here such as Interval, YearWeek, YearQuarter, and more.

Basil Bourque
  • 303,325
  • 100
  • 852
  • 1,154
  • ZonedDateTime startDT = rs.getObject("datetime", Instant.class); - will not compile for me – AnKing Jun 08 '17 at 19:46
  • @AnKing See: [ResultSet::getObject(String columnLabel, Class type)](https://docs.oracle.com/javase/8/docs/api/java/sql/ResultSet.html#getObject-java.lang.String-java.lang.Class-) – Basil Bourque Jun 08 '17 at 20:20
  • @AnKing, which Java version are you using? The overloaded generic `getObject(String, Class)` was introduced in Java 7. – Ole V.V. Jun 08 '17 at 20:33
  • when i do this: Instant zdt = rs.getObject("start", Instant.class); JVM comes up with: java.sql.SQLException: Conversion not supported for type java.time.Instant – AnKing Jun 09 '17 at 14:16
  • I'm really confused with this now: Time I got stored in Database (datetime field): 2017-06-13 11:15:00 System.out.println(rs.getTimestamp("start")); //outputs 2017-06-13 11:15:00.0 System.out.println(rs.getTimestamp("start").toInstant()); //outputs 2017-06-13T15:15:00Z So, even if Timestamp.toString() is already showing time with local offset, then why rs.getTimestamp() adds local offset when time is fetched from database? And what will be the workaround for this? – AnKing Jun 09 '17 at 14:53
  • 1
    The SQLException sounds to me like you haven’t got a JDBC 4.2 driver yet since this should be the version where conversion for `Instant` begins. I may be repeating something that has already been said, a good solution would be getting such a new driver. – Ole V.V. Jun 09 '17 at 15:26
  • @AnKing Please reread my Answer. Carefully consider the example code and output. You missed the main point about **not believing the output of `toString`** method on the troublesome old legacy date-time classes. Your statement, "Time I got stored in Database (datetime field): 2017-06-13 11:15:00", is **not true**. You have 2017-06-13 15:15:00 Z (UTC) stored (apparently). You must understand the difference between *data model* (the internals of your stored date-time value in database and in date-time object) and *presentation* (generating a String to represent value). – Basil Bourque Jun 09 '17 at 17:21
  • @BasilBourque Regarding Timestamp and Instant being UTC and toString lying... My Timestamp shows (toString) in the debugger: 2020-04-27 08:00:18; The instant 2020-04-27T06:00:18Z So a two hours offset. Looking into the cdate of the timestamp reveals it has a 2h offset. – Werner Daehn Apr 27 '20 at 10:36
  • Okay, I can confirm that getTimestamp() does not return UTC by default but the Timezone.getDefault(). Proof1: Modify the Timezone.setDefault() and getTimestamp.toInstant() will return different values. Proof2: Do not use getTimestamp(index) but getTimestamp(index, calendar) to control the timezone assigned to the timestamp. – Werner Daehn Apr 27 '20 at 11:48
2

The old classes like java.sql.Timestamp have quite some issues with their design. One of the things that often cause confusion is that Timestamp.toString() prints the time in the JVM’s time zone even though the time Timestamp in it just holds a point in time with no time zone. To be concrete, when a Timestamp is equal to 2017-06-08T17:15:00Z in UTC and you print it on a computer running Pittsburg time (which at this time of year has an offset of -4:00 from UTC), the Timestamp.toString() is implicitly called, it reads the JVM’s time zone and prints the time as 13:15:00.0 just because this time in Pennsylvania is equal to the UTC time in the Timestamp.

So to cut a long story short, do not worry, both your Timestamp and your Instant are correct.

Ole V.V.
  • 81,772
  • 15
  • 137
  • 161
  • I guess my question now is: how do I fetch a "datetime" field from the database and convert it to ZonedDateTime object assuming that DB has a UTC time in it without adding any offsets during these operations? – AnKing Jun 09 '17 at 14:22
  • And I guess the answer is: you do as you do in the question. – Ole V.V. Jun 09 '17 at 15:08
  • 1
    But if I do as in question my time: 2017-06-13 11:15:00 (UTC) stored in database end up being 2017-06-13T15:15Z[Etc/UTC] (4 hour offset added). – AnKing Jun 09 '17 at 15:16
  • Weird. The best explanation I can find is if you get the wrong timestamp from `rs.getTimestamp("start")` (a timestamp that looks deceivingly right because of the weird `Timestamp.toString()` behavior). – Ole V.V. Jun 09 '17 at 15:23
  • Since there is no answer to why rs.getTimestamp("start") adds an offset to the time stored, is there a way to minus out local offset explicitly as a workaround? If this will not work I'll start looking into JDBC 4.2 driver thing. Is this just a library i will have to connect to my NetBeans project? – AnKing Jun 09 '17 at 15:49
  • 1
    It’s easy to compensate for the bug that gives you the wrong timestamp, but before doing that, you would really want to try to understand why it happens in the first place. Are you sure the value the MySQL database is showing you is in UTC, for example? – Ole V.V. Jun 09 '17 at 16:05
  • 1
    I'm using MySQL workbench to see DB tables. When I record the time: 06/13/2017 06:15 AM, I first parse it like this: ZonedDateTime.parse(jFormattedTextField1.getText(), Globals.dtFormat) (DateTimeFormatter dtFormat = DateTimeFormatter .ofPattern("MM/dd/yyyy hh:mm a") .withZone(LOCALZONEID);) Then I format it to UTC with DateTimeFormatter of UTCZONEID And record it to database. It shows 2017-06-13 10:15:00 in the database (as anticipated) – AnKing Jun 09 '17 at 16:16
0

I think this code snippet answers your question. This takes in a String in a local time zone, converts it to UTC, and stores it in a db.

    //Getting the LocalDateTime Objects from String values
    DateTimeFormatter df = DateTimeFormatter.ofPattern("yyyy-MM-dd kk:mm"); 
    String txtStartTime = "2017-03-29 12:00";

    LocalDateTime ldtStart = LocalDateTime.parse(txtStartTime, df);


    //Convert to a ZonedDate Time in UTC
    ZoneId zid = ZoneId.systemDefault();

    ZonedDateTime zdtStart = ldtStart.atZone(zid);
    System.out.println("Local Time: " + zdtStart);
    ZonedDateTime utcStart = zdtStart.withZoneSameInstant(ZoneId.of("UTC"));
    System.out.println("Zoned time: " + utcStart);
    ldtStart = utcStart.toLocalDateTime();
    System.out.println("Zoned time with zone stripped:" + ldtStart);
    //Create Timestamp values from Instants to update database
    Timestamp startsqlts = Timestamp.valueOf(ldtStart); //this value can be inserted into database
    System.out.println("Timestamp to be inserted: " +startsqlts);

    //insertDB(startsqlts);
Shoikana
  • 595
  • 4
  • 8