简体   繁体   中英

Crazy MS SQL Server JDBC driver at datetime field or time zone issue?

During debugging I end with simple example:

select convert(datetime, '2015-04-15 03:30:00') as ts
go

Apr 15 2015 03:30AM
(1 row affected)

select convert(int, datediff(second, '1970-01-01 00:00:00',
                    convert(datetime, '2015-04-15 03:30:00'))) as ts
go

1429068600
(1 row affected)

$ TZ=UTC date --date=@1429068600 +%F_%T
2015-04-15_03:30:00

When I perform query from JDBC I get 2 different results not equal to above!!!! Code:

String TEST_QUERY = "select convert(datetime, '2015-04-15 03:30:00') as ts";
PreparedStatement stmt2 = conn.prepareStatement(TEST_QUERY);
ResultSet rs = stmt2.executeQuery();
rs.next();
logger.info("getTimestamp().getTime(): {}", rs.getTimestamp("ts").getTime());
logger.info("getDate().getTime(): {}", rs.getDate("ts").getTime());
stmt2.close();

Execution result (I double-check result with Coreutils date utility):

=> getTimestamp().getTime(): 1429057800000

$ TZ=UTC date --date=@1429057800 +%F_%T
2015-04-15_00:30:00

=> getDate().getTime(): 1429045200000

$ TZ=UTC date --date=@1429045200 +%F_%T
2015-04-14_21:00:00

Official docs for date types and JDBC Java mapping say nothing about resulted difference...

My program executed in GMT+03:00 timezone and I have SQL Server 2008 and try with JDBC driver 4.0 and 4.1 from https://msdn.microsoft.com/en-us/sqlserver/aa937724.aspx

My expectation to get UTC timestamp (from 1970) in all cases, which is true only for Linux ODBC tsql utility which I use to interactively debug queries.

WTF?

Your first pair of queries compute the number of seconds since local midnight on the (local) date of the epoch. This difference is the same in any time zone, so when you set the database timezone to UTC and convert the previously-determined offset back to a timestamp you get the "same" date and time in the sense that the numbers match, but they represent a different absolute time because they are relative to a different TZ.

When you execute your query through JDBC, you are computing a Timestamp in the database timezone, GMT+03:00. java.sql.Timestamp represents absolute time, however, expressed as an offset from midnight GMT at the turn of the epoch. The JDBC driver knows how to compensate. What you then log, therefore, is the time difference between 1970-01-01 00:00:00 GMT+00:00 and 2015-04-15 03:30:00 GMT+03:00 .

The getDate().getTime() version is a little less clear cut, but it appears that when you retrieve the timestamp as a Date , thereby truncating the time part, the truncation is being performed relative to the database time zone. Afterward, and similarly to the other case, java.sql.Date.getTime() returns an offset from the turn of the epoch to the resulting absolute time. That is, it is computing the difference between 1970-01-01 00:00:00 GMT+00:00 and 2015-04-15 00:00:00 GMT+03:00

This is all consistent.

With help of JD-GUI I investigate SQL Server JDBC binary.

rs.getTimestamp() method lead to:

package com.microsoft.sqlserver.jdbc;
final class DDC {

static final Object convertTemporalToObject(
       JDBCType paramJDBCType, SSType paramSSType, Calendar paramCalendar, int paramInt1, long paramLong, int paramInt2) {
    TimeZone localTimeZone1 = null != paramCalendar ? 
         paramCalendar.getTimeZone() : TimeZone.getDefault();
    TimeZone localTimeZone2 = SSType.DATETIMEOFFSET == paramSSType ? 
         UTC.timeZone : localTimeZone1;

    Object localObject1 = new GregorianCalendar(localTimeZone2, Locale.US);

    ...

     ((GregorianCalendar)localObject1).set(1900, 0, 1 + paramInt1, 0, 0, 0);
        ((GregorianCalendar)localObject1).set(14, (int)paramLong);

which called from resultset reader of TDS protocol:

package com.microsoft.sqlserver.jdbc;

final class TDSReader {

final Object readDateTime(int paramInt, Calendar paramCalendar, JDBCType paramJDBCType, StreamType paramStreamType)
  throws SQLServerException
{
  ...
  switch (paramInt)
  {
  case 8:
    i = readInt();
    j = readInt();

    k = (j * 10 + 1) / 3;
    break;
  ...
  return DDC.convertTemporalToObject(paramJDBCType, SSType.DATETIME, paramCalendar, i, k, 0);
}

So two 4 bytes words for datetime represent days from 1900 and milliseconds in day and that data set to Calendar .

It is hard to check from where comes Calendar . But code show that it is possible that local Java TZ in use (look to TimeZone.getDefault() ).

If we assume that DB holds time from 1900 in UTC you need to apply TZ correction to get long getTimestamp().getTime() result from JDBC driver in UTC on Java side because driver assumes local time from DB.

IMPORTANT UPDATE I take experiment with setting default locale to UTC at very beginning of my application:

TimeZone.setDefault(TimeZone.getTimeZone("GMT+00:00"));

and get UTC ms from getTimestamp().getTime() . That was a win! I can avoid monstrous SQL Server date arithmetic to get seconds from 1970.

As mentioned in other answers, the JDBC driver will adjust the date/time to/from the default TimeZone of the JVM. This is of course only an issue when the database and the application is running in different timezones.

If you need the date/time in a different timezone, you might be able to change the default timezone of the JVM (as suggested in another answer), but that may not be possible if your code is running in an application server hosting multiple applications.

A better choice might be to explicitly specify the timezone you need, by using the overloaded method taking a Calendar object.

Connection conn = ...;
String sql = ...;
Timestamp tsParam = ...;

// Get Calendar for UTC timezone
Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("UTC"));

try (PreparedStatement stmt = conn.prepareStatement(sql)) {

    // Send statement parameter in UTC timezone
    stmt.setTimestamp(1, tsParam, cal);

    try (ResultSet rs = stmt.executeQuery()) {
        while (rs.next()) {

            // Get result column in UTC timezone
            Timestamp tsCol = rs.getTimestamp("...", cal);

            // Use tsCol here
        }
    }
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM