简体   繁体   English

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

[英]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!!!!当我从 JDBC 执行查询时,我得到 2 个不等于上面的不同结果!!!! 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):执行结果(我使用 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

Official docs for date types and JDBC Java mapping say nothing about resulted difference... 日期类型和 JDBC Java 映射的官方文档没有说明结果差异......

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我的程序在GMT+03:00时区执行,我有 SQL Server 2008 并尝试使用来自https://msdn.microsoft.com/en-us/sqlserver/aa937724.aspx 的JDBC 驱动程序 4.0 和 4.1

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

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.这种差异在任何时区都是相同的,因此当您将数据库时区设置为 UTC 并将先前确定的偏移量转换回时间戳时,您将获得“相同”的日期和时间,因为数字匹配,但它们代表不同的绝对时间,因为它们相对于不同的 TZ。

When you execute your query through JDBC, you are computing a Timestamp in the database timezone, GMT+03:00.当您通过 JDBC 执行查询时,您正在计算数据库时区中的Timestamp ,GMT+03:00。 java.sql.Timestamp represents absolute time, however, expressed as an offset from midnight GMT at the turn of the epoch. java.sql.Timestamp表示绝对时间,但是,表示为在纪元之交时格林威治标准时间午夜的偏移量。 The JDBC driver knows how to compensate. JDBC 驱动程序知道如何进行补偿。 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 .因此,您随后记录的是1970-01-01 00:00:00 GMT+00:002015-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. getDate().getTime()版本不太清楚,但似乎当您将时间戳作为Date检索时,从而截断时间部分,截断是相对于数据库时区执行的。 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.之后,与另一种情况类似, java.sql.Date.getTime()返回从纪元轮到结果绝对时间的偏移量。 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也就是说,它正在计算1970-01-01 00:00:00 GMT+00:002015-04-15 00:00:00 GMT+03:00之间的差异

This is all consistent.这都是一致的。

With help of JD-GUI I investigate SQL Server JDBC binary.JD-GUI帮助下,我调查了 SQL Server JDBC 二进制文件。

rs.getTimestamp() method lead to: 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);

which called from resultset reader of TDS protocol:从 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);
}

So two 4 bytes words for datetime represent days from 1900 and milliseconds in day and that data set to Calendar .因此datetime两个 4 字节单词表示从 1900 年开始的天数和一天中的毫秒数,并且该数据设置为Calendar

It is hard to check from where comes Calendar .很难检查Calendar是从哪里来的。 But code show that it is possible that local Java TZ in use (look to TimeZone.getDefault() ).但是代码显示可能正在使用本地 Java TZ(查看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.如果我们假设 DB 在 UTC 中保持从 1900 年开始的时间,则您需要应用 TZ 校正以从 Java 端的 UTC 中的 JDBC 驱动程序获得long getTimestamp().getTime()结果,因为驱动程序假定来自 DB 的本地时间。

IMPORTANT UPDATE I take experiment with setting default locale to UTC at very beginning of my application:重要更新我在我的应用程序开始时尝试将默认语言环境设置为 UTC:

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

and get UTC ms from getTimestamp().getTime() .并从getTimestamp().getTime()获取 UTC 毫秒。 That was a win!那是一场胜利! I can avoid monstrous SQL Server date arithmetic to get seconds from 1970.我可以避免使用可怕的 SQL Server 日期算法来获取 1970 年的秒数。

As mentioned in other answers, the JDBC driver will adjust the date/time to/from the default TimeZone of the JVM.正如其他答案中提到的,JDBC 驱动程序将调整日期/时间到/从 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.如果您需要不同时区的日期/时间,您可能能够更改 JVM 的默认时区(如另一个答案中所建议的那样),但如果您的代码在托管多个应用程序的应用程序服务器中运行,则这可能是不可能的.

A better choice might be to explicitly specify the timezone you need, by using the overloaded method taking a Calendar object.更好的选择可能是通过使用采用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
        }
    }
}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

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