Crazy MS SQL 服务器 JDBC 日期时间字段或时区问题的驱动程序?
Crazy MS SQL Server JDBC driver at datetime field or time zone issue?
在调试过程中,我以简单的例子结束:
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
Official docs for date types and JDBC Java mapping 什么都不说结果差异...
我的程序在 GMT+03:00
时区执行,我有 SQL Server 2008 并尝试使用来自 https://msdn.microsoft.com/en-us/sqlserver/aa937724.aspx
的 JDBC 驱动程序 4.0 和 4.1
我期望在所有情况下都获得 UTC 时间戳(从 1970 年开始),这仅适用于我用来交互式调试查询的 Linux ODBC tsql
实用程序。
卧槽?
您的第一对查询计算自纪元的(本地)日期本地午夜以来的秒数。这种差异在任何时区都是相同的,因此当您将数据库时区设置为 UTC 并将 previously-determined 偏移量转换回时间戳时,您会在数字匹配的意义上得到 "same" 日期和时间, 但它们代表不同的绝对时间,因为它们是相对于不同的 TZ。
当您通过 JDBC 执行查询时,您正在计算数据库时区 GMT+03:00 中的 Timestamp
。 java.sql.Timestamp
表示绝对时间,但是表示为从午夜 GMT 到纪元之交的偏移量。 JDBC driver 懂得弥补。因此,您随后记录的是 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()
returns 从纪元开始到最终绝对时间的偏移量。也就是说,它正在计算 1970-01-01 00:00:00 GMT+00:00
和 2015-04-15 00:00:00 GMT+03:00
之间的差异
这都是一致的。
在 JD-GUI
的帮助下,我调查了 SQL 服务器 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 协议的结果集 reader 调用:
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()
)。
如果我们假设 DB 从 1900 年开始保持 UTC 时间,您需要应用 TZ 校正以从 Java 端的 UTC JDBC 驱动程序获得 long getTimestamp().getTime()
结果,因为驱动程序假设本地来自 DB 的时间。
重要更新 我在应用程序的最开始尝试将默认语言环境设置为 UTC:
TimeZone.setDefault(TimeZone.getTimeZone("GMT+00:00"));
并从 getTimestamp().getTime()
获取 UTC 毫秒。那是一场胜利!我可以避免可怕的 SQL 服务器日期算法来获取 1970 年的秒数。
如其他答案中所述,JDBC 驱动程序将调整 date/time to/from JVM 的默认时区。当数据库和应用程序处于不同时区时,这当然只是一个问题。
如果您需要 date/time 在不同的时区,您可以更改 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
}
}
}
在调试过程中,我以简单的例子结束:
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
Official docs for date types and JDBC Java mapping 什么都不说结果差异...
我的程序在 GMT+03:00
时区执行,我有 SQL Server 2008 并尝试使用来自 https://msdn.microsoft.com/en-us/sqlserver/aa937724.aspx
我期望在所有情况下都获得 UTC 时间戳(从 1970 年开始),这仅适用于我用来交互式调试查询的 Linux ODBC tsql
实用程序。
卧槽?
您的第一对查询计算自纪元的(本地)日期本地午夜以来的秒数。这种差异在任何时区都是相同的,因此当您将数据库时区设置为 UTC 并将 previously-determined 偏移量转换回时间戳时,您会在数字匹配的意义上得到 "same" 日期和时间, 但它们代表不同的绝对时间,因为它们是相对于不同的 TZ。
当您通过 JDBC 执行查询时,您正在计算数据库时区 GMT+03:00 中的 Timestamp
。 java.sql.Timestamp
表示绝对时间,但是表示为从午夜 GMT 到纪元之交的偏移量。 JDBC driver 懂得弥补。因此,您随后记录的是 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()
returns 从纪元开始到最终绝对时间的偏移量。也就是说,它正在计算 1970-01-01 00:00:00 GMT+00:00
和 2015-04-15 00:00:00 GMT+03:00
这都是一致的。
在 JD-GUI
的帮助下,我调查了 SQL 服务器 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 协议的结果集 reader 调用:
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()
)。
如果我们假设 DB 从 1900 年开始保持 UTC 时间,您需要应用 TZ 校正以从 Java 端的 UTC JDBC 驱动程序获得 long getTimestamp().getTime()
结果,因为驱动程序假设本地来自 DB 的时间。
重要更新 我在应用程序的最开始尝试将默认语言环境设置为 UTC:
TimeZone.setDefault(TimeZone.getTimeZone("GMT+00:00"));
并从 getTimestamp().getTime()
获取 UTC 毫秒。那是一场胜利!我可以避免可怕的 SQL 服务器日期算法来获取 1970 年的秒数。
如其他答案中所述,JDBC 驱动程序将调整 date/time to/from JVM 的默认时区。当数据库和应用程序处于不同时区时,这当然只是一个问题。
如果您需要 date/time 在不同的时区,您可以更改 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
}
}
}