在日期时间字段或时区问题疯狂的MS SQL Server JDBC驱动程序?

在调试期间,我以简单的例子结束:

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 

当我从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 --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 

日期类型和JDBC Java映射的官方文档没有说明产生的差异……

我的程序在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?

您的第一对查询计算自纪元(本地)日期当地午夜以来的秒数。 这种差异在任何时区都是相同的,因此当您将数据库时区设置为UTC并将先前确定的偏移量转换回时间戳时,您会得到与数字匹配的“相同”日期和时间,但它们代表不同的绝对时间,因为它们是相对于不同的TZ。

通过JDBC执行查询时,您将在数据库时区GMT + 03:00中计算Timestamp 。 但是, java.sql.Timestamp表示绝对时间,表示为从纪元转折处的格林威治标准时间午夜开始的偏移量。 JDBC驱动程序知道如何补偿。 因此,您记录的是1970-01-01 00:00:00 GMT+00:002015-04-15 03:30:00 GMT+03:00

getDate().getTime()版本稍微不那么明确,但是当您将时间戳检索为Date ,从而截断时间部分时,会相对于数据库时区执行截断。 之后,与其他情况类似, java.sql.Date.getTime()返回从纪元转向到结果绝对时间的偏移量。 也就是说,它计算1970-01-01 00:00:00 GMT+00:002015-04-15 00:00:00 GMT+03:00之间的差异

这一切都是一致的。

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

因此, datetime两个4字节单词表示从1900开始的天数和白天的毫秒数,并且该数据设置为Calendar

Calendar到哪里检查很难。 但代码显示有可能使用本地Java TZ(查看TimeZone.getDefault() )。

如果总结该信息,则可以认为JDBC驱动程序是纯编写的。 如果认为数据库在UTC中保留1900年的时间,则需要应用TZ校正来获取Java端的UTC驱动程序中的JDBC驱动程序的long getTimestamp().getTime()结果,因为驱动程序假定他从数据库获取本地时间。

重要更新我尝试在应用程序的最开始时将默认语言环境设置为UTC:

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

并从getTimestamp().getTime()获取UTC ms。 那是胜利! 从1970年开始,我可以避免骇人听闻的SQL Server日期算术。

如其他答案中所述,JDBC驱动程序将调整JVM的默认TimeZone的日期/时间。 当数据库和应用程序在不同的时区运行时,这当然只是一个问题。

如果您需要不同时区的日期/时间,您可能能够更改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 } } }