Сумасшедший драйвер JDBC MS SQL Server в поле даты или времени?

Во время отладки я заканчиваю простым примером:

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 [email protected] +%F_%T
2015-04-15_03:30:00

Когда я выполняю запрос от JDBC, я получаю 2 разных результата, не равных выше!!!! Код:

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();

Результат выполнения (я дважды проверяю результат с помощью утилиты Coreutils date):

=> getTimestamp().getTime(): 1429057800000

$ TZ=UTC date [email protected] +%F_%T
2015-04-15_00:30:00

=> getDate().getTime(): 1429045200000

$ TZ=UTC date [email protected] +%F_%T
2015-04-14_21:00:00

Официальные документы для типов дат и отображения Java JDBC не говорят о приведенной разнице...

Моя программа выполнена в часовом поясе GMT+03:00, и у меня есть SQL Server 2008 и попробуйте с помощью JDBC-драйвера 4.0 и 4.1 из https://msdn.microsoft.com/en-us/sqlserver/aa937724.aspx

Мое ожидание получить временную метку UTC (с 1970 года) во всех случаях, что верно только для утилиты Linux ODBC tsql, которую я использую для интерактивного отладки запросов.

WTF?

Ответ 1

Ваша первая пара запросов вычисляет количество секунд с местной полуночи в (местную) дату эпохи. Это различие одинаково в любом часовом поясе, поэтому, когда вы устанавливаете часовой пояс базы данных в UTC и конвертируете ранее установленное смещение обратно в метку времени, вы получаете "ту же" дату и время в том смысле, что цифры совпадают, но они представляют другое абсолютное время, потому что они относятся к другому TZ.

Когда вы выполняете свой запрос через JDBC, вы вычисляете Timestamp в часовой пояс базы данных, GMT + 03: 00. java.sql.Timestamp представляет абсолютное время, однако, выраженное как смещение от полуночи GMT на рубеже эпохи. Драйвер JDBC знает, как компенсировать. Таким образом, тогда вы регистрируете разницу во времени между 1970-01-01 00:00:00 GMT+00:00 и 2015-04-15 03:30:00 GMT+03:00.

Версия getDate().getTime() немного менее четкая, но кажется, что когда вы извлекаете временную метку как Date, тем самым сокращая временную часть, выполняется усечение относительно временного пояса базы данных. После этого и аналогично другому случаю java.sql.Date.getTime() возвращает смещение от поворота эпохи до конечного абсолютного времени. То есть он вычисляет разницу между 1970-01-01 00:00:00 GMT+00:00 и 2015-04-15 00:00:00 GMT+03:00

Все это согласовано.

Ответ 2

С помощью JD-GUI я исследую двоичный код SQL Server JDBC.

rs.getTimestamp() метод приводит к:

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);

который вызывается из получателя результатов из протокола TDS:

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);
}

Итак, два слова из 4 байтов для datetime представляют дни с 1900 года и миллисекунды в день, и эти данные установлены на Calendar.

Трудно проверить, откуда идет Calendar. Но код показывает, что возможно использование локальной Java TZ (смотрите TimeZone.getDefault()).

Если суммировать эту информацию, можно подумать, что драйверы JDBC чисты. Если вы думаете, что время ожидания DB с 1900 в UTC, вам нужно применить коррекцию TZ, чтобы получить результат long getTimestamp().getTime() от драйвера JDBC в UTC на стороне Java, потому что драйвер предполагает, что он получает локальное время из БД.

ВАЖНОЕ ОБНОВЛЕНИЕ Я беру эксперимент с установкой стандартного языка в UTC в самом начале моего приложения:

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

и получить UTC ms из getTimestamp().getTime(). Это была победа! Я могу избежать чудовищной арифметики даты SQL Server, чтобы получить секунды с 1970 года.

Ответ 3

Как упоминалось в других ответах, драйвер JDBC будет настраивать дату/время на/из заданного по умолчанию TimeZone JVM. Это, конечно, только проблема, когда база данных и приложение работают в разных часовых поясах.

Если вам нужна дата/время в другом часовом поясе, вы можете изменить часовой пояс по умолчанию JVM (как предлагается в другом ответе), но это может быть невозможно, если ваш код запущен на сервере приложений хостинг нескольких приложений.

Лучшим выбором может быть явное указание требуемого часового пояса, используя перегруженный метод с объектом Calendar.

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
        }
    }
}