背景
時間瞬間是時間線上的一個特定時刻。當時間瞬間的值在儲存到資料庫或從資料庫擷取時,無論資料庫伺服器和用戶端在什麼時區中運作,它始終指向時間上的同一個點時,就說時間瞬間被保留了。
TIMESTAMP
是唯一設計用於儲存瞬間的 MySQL 資料類型。為了保留時間瞬間,伺服器會在需要時,對傳入或傳出的時間值套用時區轉換。傳入的值會由伺服器從連線工作階段的時區轉換為國際標準時間 (UTC) 以進行儲存,而傳出的值則從 UTC 轉換為工作階段時區。從 MySQL 8.0.19 開始,您也可以在儲存 TIMESTAMP
值時指定時區偏移量 (如需詳細資訊,請參閱DATE、DATETIME 和 TIMESTAMP 類型),在這種情況下,TIMESTAMP
值會從指定的偏移量而非工作階段時區轉換為 UTC。但是,一旦儲存,原始的偏移量資訊將不再保留。
對於 DATETIME
資料類型,情況不太直接:它不代表瞬間,而且在未指定時區偏移量時,DATETIME
值不會進行時區轉換,因此它們會按原樣儲存和擷取。然而,如果指定時區偏移量,輸入值會在儲存之前轉換為工作階段時區;結果是,當在不同工作階段中擷取時,若該工作階段的時區偏移量與指定的不同,DATETIME
值會與原始輸入值不同。
由於 MySQL 資料類型 (而非 TIMESTAMP
) (以及那些其他 MySQL 資料類型的 Java 包裝類別) 不代表真正時間瞬間;因此,在儲存和擷取值時混合使用代表瞬間和不代表瞬間的日期時間類型可能會導致意外的結果。例如:
當將
java.sql.Timestamp
儲存到例如DATETIME
資料行時,當在與儲存該值時用戶端所在時區不同的時區中,將其擷取到用戶端時,您可能無法取得相同的瞬間值。當將例如
java.time.LocalDateTime
儲存到TIMESTAMP
資料行時,您可能不會為其儲存正確的 UTC 型值,因為該值的時區實際上是未定義的。
因此,當與伺服器互動時,請勿將瞬間日期時間類型 (java.util.Calendar
、java.util.Date
、java.time.OffsetDateTime
、java.sql.Timestamp
) 傳遞至非瞬間日期時間類型 (例如,java.sql.DATE
、java.time.LocalDate
、java.time.LocalTime
、java.time.OffsetTime
),反之亦然。
本節的其餘部分將討論在使用 Connector/J 時如何保留時間瞬間。
使用 Connector/J 保留瞬間
情境: 假設應用程式正在某個應用程式伺服器上執行,並且使用 Connector/J 連線到 MySQL 伺服器。連線工作階段中會發生某些事件,並產生時間戳記,且事件時間戳記會與應用程式伺服器的 JVM 時區相關聯。這些時間戳記會儲存到 MySQL 伺服器,並稍後從中擷取。
挑戰: 當使用 Connector/J 將時間戳記儲存到伺服器或從伺服器擷取時,需要保留時間戳記的瞬間值。由於 MySQL 伺服器始終隱含地假設,當儲存到伺服器或從伺服器擷取時,時間瞬間值會參考連線工作階段時區 (由工作階段的time_zone
變數設定),因此只有在以下情況下才能正確保留時間瞬間值:
當 Connector/J 與 MySQL 伺服器在同一時區中執行時 (亦即,伺服器的工作階段時區與 JVM 的時區相同),時間瞬間自然會被保留,並且不需要時區轉換。請注意,在這種情況下,只有當伺服器和 JVM 在未來始終在同一時區中執行時,才會真正保留時間瞬間。
-
當 Connector/J 在與 MySQL 伺服器不同的時區中執行時 (亦即,JVM 的時區與伺服器的工作階段時區不同),Connector/.J 會執行下列其中一項操作:
從伺服器查詢工作階段時區的值,並在工作階段時區和 JVM 時區之間轉換事件時間戳記。
將伺服器的工作階段時區變更為 JVM 的時區,之後就不需要時區轉換。
將伺服器工作階段時區變更為使用者指定的所需時區,然後在 JVM 時區和使用者指定的時區之間轉換時間戳記。
我們將上述針對時間點保存的解決方案定義為方案 1、2a、2b 和 2c。為了實現這些解決方案,自 8.0.23 版本以來,Connector/J 中引入了以下連線屬性:
-
preserveInstants={true|false}
:是否嘗試透過調整時間戳記來保存時間點值。-
當其為
false
時,不會嘗試進行轉換;時間戳記會原樣傳送到伺服器進行儲存,並且會保存其視覺呈現方式,而非實際的時間點。當 Connector/J 從伺服器擷取時間戳記時,可能會與不同的時區關聯,因為擷取可能會在不同的 JVM 時區中發生。例如:時區:JVM 為 UTC,伺服器工作階段為 UTC+1
來自用戶端的原始時間戳記(以 UTC 為單位):
2020-01-01 01:00:00
Connector/J 傳送到伺服器的時間戳記:
2020-01-01 01:00:00
(不轉換)內部儲存在伺服器上的時間戳記值:
2020-01-01 00:00:00 UTC
(在內部將2020-01-01 00:00:00 UTC+1
轉換為 UTC 之後)稍後擷取到伺服器區段中的時間戳記值(以 UTC+1 為單位):
2020-01-01 01:00:00
(在內部將2020-01-01 00:00:00
從 UTC 轉換為 UTC+1 之後)Connector/J 在與之前不同的其他 JVM 時區中(例如,在 UTC+3 中)建構的時間戳記值:
2020-01-01 01:00:00
註解:時間點未保存
-
當其為
true
時,Connector/J 會嘗試透過以連線屬性connectionTimeZone
和forceConnectionTimeZoneToSession
定義的方式執行轉換來保存時間點。儲存值時,僅當目標資料類型(明確的或預設的)為
TIMESTAMP
時,才會執行轉換。擷取值時,僅當來源欄具有TIMESTAMP
、DATETIME
或字元資料類型,且目標類別是像java.sql.Timestamp
或java.time.OffsetDateTime
這種保存時間點的類別時,才會執行轉換。
-
-
connectionTimeZone={LOCAL|SERVER|
:指定 Connector/J 如何判斷伺服器的工作階段時區(時間戳記會參考該時區儲存到伺服器上)。它會採用下列其中一個值:user-defined-time-zone
}LOCAL
:Connector/J 假設伺服器的工作階段時區 (a) 與 Connector/J 的 JVM 時區相同,或 (b) 應設定為與 Connector/J 的 JVM 時區相同。Connector/J 會根據連線屬性forceConnectionTimeZoneToSession
的值,將情況視為 (a) 或 (b)。SERVER:Connector/J 應從伺服器查詢工作階段的時區,而不是對其進行任何假設。如果工作階段時區實際上與 Connector/J 的 JVM 時區不同,且
preserveInstants=true
,則 Connector/J 會在工作階段時區和 JVM 時區之間執行時區轉換。user-defined-time-zone
:Connector/J 假設伺服器的工作階段時區 (a) 與使用者定義的時區相同,或 (b) 應設定為使用者定義的時區。Connector/J 會根據連線屬性forceConnectionTimeZoneToSession
的值,將情況視為 (a) 或 (b)。
注意對於 Connector/J 8.0.23 及更新版本,
serverTimezone
是connectionTimeZone
的別名。對於 Connector/J 8.0.22 及更早版本,serverTimezone
用於覆寫伺服器上的工作階段時區設定。 forceConnectionTimeZoneToSession={true|false}
:控制是否應將工作階段time_zone
變數設定為connectionTimeZone
中指定的值。
現在,以下是用於實現上述定義的解決方案以保存時間點的連線屬性值:
-
方案 1:使用 preserveInstants=false 或 connectionTimeZone=LOCAL& forceConnectionTimeZoneToSession=false。因為可以安全地假設伺服器工作階段時區與 Connector/J 的 JVM 時區相同,所以不會發生對伺服器工作階段時區的查詢,也不會發生時區轉換。例如:
時區:JVM 和伺服器工作階段都為 UTC+1
來自用戶端的原始時間戳記(以 UTC+1 為單位):
2020-01-01 01:00:00
Connector/J 傳送到伺服器的時間戳記:
2020-01-01 01:00:00
(不需要轉換)內部儲存在伺服器上的時間戳記值:
2020-01-01 00:00:00 UTC
(在內部從 UTC+1 轉換為 UTC 之後)稍後擷取到 Connector/J 連線的 UTC+1 伺服器時間工作階段中的時間戳記值:
2020-01-01 01:00:00
(在內部從 UTC 轉換為 UTC+1 之後)Connector/J 在與之前相同的 JVM 時區 (UTC+1) 中建構並傳回應用程式的時間戳記值:
2020-01-01 01:00:00
註解:時間點在沒有轉換的情況下保存。
注意此設定對應於 Connector/J 5.1 的預設行為
-
方案 2a:使用 preserveInstants=true&connectionTimeZone=SERVER 。然後,Connector/J 會從伺服器查詢工作階段時區的值,並在工作階段時區和 JVM 時區之間轉換事件時間戳記。例如:
時區:JVM 為 UTC+2,伺服器工作階段為 UTC+1
來自用戶端的原始時間戳記(以 UTC+2 為單位):
2020-01-01 02:00:00
Connector/J 傳送到伺服器的時間戳記:
2020-01-01 01:00:00
(從 UTC+2 轉換為 UTC+1 之後)內部儲存在伺服器上的時間戳記值:
2020-01-01 00:00:00 UTC
(在內部從 UTC+1 轉換為 UTC 之後)稍後擷取到 UTC+1 伺服器工作階段中的時間戳記值:
2020-01-01 01:00:00
(在內部從 UTC 轉換為 UTC+1 之後)Connector/J 在與之前相同的 JVM 時區 (UTC+2) 中建構並傳回應用程式的時間戳記值:
2020-01-01 02:00:00
(從 UTC+1 轉換為 UTC+2 之後)Connector/J 在另一個 JVM 時區(例如,UTC+3)中建構並傳回應用程式的時間戳記值:
2020-01-01 03:00:00
(從 UTC+1 轉換為 UTC+3 之後)註解:時間點已保存。
注意事項此設定對應於 Connector/J 8.0.22 及更早版本的預設行為,以及
useLegacyDatetimeCode=false
的 Connector/J 5.1 行為。
-
方案 2b:使用 connectionTimeZone=LOCAL& forceConnectionTimeZoneToSession=true。然後,Connector/J 會將伺服器的工作階段時區變更為 JVM 時區,之後在儲存或取得時間戳記時不需要進行時區轉換。例如:
時區:JVM 為 UTC+1,伺服器工作階段原本為 UTC+2,但現在由 Connector/J 修改為 UTC+1
來自用戶端的原始時間戳記(以 UTC+1 為單位):
2020-01-01 01:00:00
Connector/J 傳送到伺服器的時間戳記:
2020-01-01 01:00:00
(不轉換)內部儲存在伺服器上的時間戳記值:
2020-01-01 00:00:00
(在內部從 UTC+1 轉換為 UTC 之後)稍後擷取到伺服器工作階段中的時間戳記值(由 Connector/J 設定為 UTC+1):
2020-01-01 01:00:00
(在內部從 UTC 轉換為 UTC+1 之後)Connector/J 在與之前相同的 JVM 時區 (UTC+1) 中建構的時間戳記值:
2020-01-01 01:00:00
(不需要轉換)稍後擷取到伺服器工作階段中的時間戳記值(時區由 Connector/J 修改為,例如,UTC+3):
2020-01-01 03:00:00
(在內部從 UTC 轉換為 UTC+3 之後)Connector/J 在 UTC+3 的 JVM 時區中建構的時間戳記值:
2020-01-01 03:00:00
(不需要轉換)註解:時間點在沒有 Connector/J 轉換的情況下保存,因為工作階段時區由 Connector/J 變更為其 JVM 的值。
警告-
變更工作階段時區會影響 MySQL 函數(例如
NOW()
、CURTIME()
或CURDATE()
)的結果,如果您不希望這些函數受到影響,請勿使用此設定。如果您在不同時區的不同用戶端上使用此設定,則用戶端會將其連線工作階段的時區修改為不同的值;如果您想讓所有用戶端及其所有工作階段中,同一個時間點的視覺日期時間值表示保持相同,請將值儲存到
DATETIME
而不是TIMESTAMP
欄,並為它們使用非時間點 Java 類別,例如java.time.LocalDateTime
。
-
方案 2c:使用 preserveInstants=true&connectionTimeZone=
user-defined-time-zone
& forceConnectionTimeZoneToSession=true。然後,Connector/J 會將伺服器的工作階段時區變更為使用者定義的時區,並在使用者定義的時區和 JVM 時區之間轉換時間戳記。此設定的典型使用案例是,當伺服器上的工作階段時區值已知無法被 Connector/J 識別時(例如,CST
或CEST
)。例如:時區:JVM 為 UTC+2,伺服器工作階段原本為
CET
,但現在由 Connector/J 修改為使用者指定的Europe/Berlin
來自用戶端的原始時間戳記(以 UTC+2 為單位):
2020-01-01 02:00:00
Connector/J 傳送到伺服器的時間戳記:
2020-01-01 01:00:00
(在 JVM 時區 (UTC+2) 和使用者定義的時區 (Europe/Berlin
=UTC+1) 之間轉換之後)內部儲存在伺服器上的時間戳記值:
2020-01-01 00:00:00
(在內部從 UTC+1 轉換為 UTC 之後)擷取到伺服器工作階段中的時間戳記值(時區由 Connector/J 修改為
Europe/Berlin
(=UTC+1)):2020-01-01 01:00:00
(在內部從 UTC 轉換為 UTC+1 之後)Connector/J 在與之前相同的 JVM 時區 (UTC+2) 中建構並傳回應用程式的時間戳記值:
2020-01-01 02:00:00
(在使用者定義的時區 (UTC+1) 和 JVM 時區 (UTC+2) 之間轉換之後)。註解:時間點在轉換的情況下保存,且工作階段時區由 Connector/J 根據使用者定義的值變更。
作為此解決方案的替代方案,使用者可能希望在 JVM 時區和使用者定義的時區之間對時間戳記進行相同的轉換,如上所述,而無需實際更正伺服器上無法識別的時區值。為此,請使用
preserveInstants=true&connectionTimeZone=user-defined-time-zone& forceConnectionTimeZoneToSession=false
。這可以達到相同的保存時間點結果。警告請參閱上面針對方案 2b 的警告。