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

Java 疯狂的MS SQL Server JDBC驱动程序在日期时间字段或时区出现问题?,java,sql-server,datetime,jdbc,Java,Sql Server,Datetime,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 142

在调试过程中,我以简单的示例结束:

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执行查询时,我得到了两个不同的结果,与上面的结果不同!!!!代码:

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
utility对结果进行了双重检查):

不要说结果的不同

我的程序在
GMT+03:00
时区执行,我有SQL Server 2008,并尝试使用JDBC驱动程序4.0和4.1

我希望在所有情况下都能获得UTC时间戳(从1970年开始),这仅适用于我用来交互调试查询的Linux ODBC
tsql
实用程序


WTF?

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

当您通过JDBC执行查询时,您正在计算数据库时区GMT+03:00中的时间戳<但是,code>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


这一切都是一致的。

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

当您通过JDBC执行查询时,您正在计算数据库时区GMT+03:00中的时间戳<但是,code>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


这一切都是一致的。

在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协议的resultset读取器调用:

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

很难从哪里来查看
日历
。但代码显示,本地Java TZ可能正在使用中(请查看
TimeZone.getDefault()

如果总结这些信息,就有可能认为JDBC驱动程序是纯编写的。如果认为DB从UTC的1900保持时间,您需要应用TZ校正,以从Java端UTC的JDBC驱动程序获取
long getTimestamp().getTime()
结果,因为驱动程序假定他从DB获取本地时间

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

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

并从
getTimestamp().getTime()
获取UTC毫秒。那就是胜利!我可以避免使用可怕的SQL Server日期算法从1970年开始计算秒数。

借助
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协议的resultset读取器调用:

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

很难从哪里来查看
日历
。但代码显示,本地Java TZ可能正在使用中(请查看
TimeZone.getDefault()

如果总结这些信息,就有可能认为JDBC驱动程序是纯编写的。如果认为DB从UTC的1900保持时间,您需要应用TZ校正,以从Java端UTC的JDBC驱动程序获取
long getTimestamp().getTime()
结果,因为驱动程序假定他从DB获取本地时间

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

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

并从
getTimestamp().getTime()
获取UTC毫秒。那就是胜利!我可以避免使用可怕的SQL Server日期算法从1970年开始计算秒数。

如其他答案中所述,JDBC驱动程序会将日期/时间调整到默认时区t