[英]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
日期類型和 JDBC Java 映射的官方文檔沒有說明結果差異......
我的程序在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 並將先前確定的偏移量轉換回時間戳時,您將獲得“相同”的日期和時間,因為數字匹配,但它們代表不同的絕對時間,因為它們相對於不同的 TZ。
當您通過 JDBC 執行查詢時,您正在計算數據庫時區中的Timestamp
,GMT+03:00。 java.sql.Timestamp
表示絕對時間,但是,表示為在紀元之交時格林威治標准時間午夜的偏移量。 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 協議的結果集讀取器調用:
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 在 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 驅動程序將調整日期/時間到/從 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
}
}
}
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.