Сумасшедший драйвер 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
}
}
}