我已經紅色了多篇文章和討論,但我仍然有一些不確定性:我不確定我是否應該使用Instant或任何其他型別來存盤預訂——在“在線預訂”的意義上(因此來自不同國家/時間的參與者區域需要在時間線上的同一時刻滿足)。我傾向于使用LocalDateTime,因為 DB 和 Backend 設定為 UTC 并且傳入的“創建預訂”json 訊息包含 ISO 8601(帶偏移量)startDateTime 和 endDateTime 欄位。
讓我們采用以下設定: 1. 資料庫(UTC、Oracle、MSSQL)、Java 11 后端(UTC、SpringBoot、Spring JDBC 模板)、Angular 13 前端。
資料來源:
- https://apiux.com/2013/03/20/5-laws-api-dates-and-times
- https://medium.com/decisionbrain/dates-time-in-modern-java-4ed9d5848a3e
- Instant 和 LocalDateTime 有什么區別?
- java.time.LocalDate 與“營業日期”的即時對比
- https://mattgreencroft.blogspot.com/2014/12/java-8-time-choosing-right-object.html
現在清楚的是:
- 在 REST API 中使用 ISO 8601
- 接受任何時區
- 將其存盤為 UTC
- 以UTC回傳
- 如果你不需要,不要浪費時間
- 盡可能少地使用“日期時間值作為從紀元開始計數”,因為除錯和記錄變得非常困難。僅將其用于技術性內容(例如 ping)。
下一個:
例如文章 2) 告訴我們:許多應用程式只能使用LocalDate、LocalTime 和 Instant撰寫,并在 UI 層添加時區。
3) 說明:所以商業應用程式開發人員最常使用Instant 和 ZonedDateTime類。幾乎所有的后端、資料庫、業務邏輯、資料持久性、資料交換都應該使用 UTC。但是為了向用戶展示,您需要調整到用戶期望的時區。這就是 ZonedDateTime 類的目的。
讓我們總結和驗證示例:
LocalTime
- 公司有一項政策,即午餐時間在其全球每個工廠的下午 12:30 開始。
LocalDate
LocalDate 時區(在單獨的 db 列中)
- 生日 - 如果知道某人當天的年齡至關重要,那么僅日期值是不夠的。只有一個約會,一個在日本東京看起來年滿 18 歲的人在美國俄亥俄州托萊多仍然是 17 歲。
- 合同中的到期日。如果僅表示日期,日本的利益相關者將認為任務過期,而托萊多的另一利益相關者將任務視為準時。
LocalDateTime
- 圣誕節從 2015 年 12 月 25 日午夜開始。
- 預約——在“本地”牙醫預約的意義上
Instant
- 某些關鍵功能必須在每天凌晨 1:30 開始(LocalDateTime 和 ZonedDateTime 在這種情況下是錯誤的,因為例如在從/到標準時間到夏令時的切換程序中,功能運行兩次/根本不運行)。
uj5u.com熱心網友回復:
商店預訂——在“在線預訂”的意義上(因此來自不同國家/時區的參與者需要在時間線上的同一時刻見面)。我傾向于使用 LocalDateTime,因為 DB 和 Backend 設定為 UTC,并且傳入的“創建預訂”json 訊息包含 ISO 8601(帶偏移量)startDateTime 和 endDateTime 欄位。
您并沒有真正定義“預訂”的含義。
如果您的意思是確定一個時刻,時間線上的一個特定點,那么使用的類LocalDateTime完全是錯誤的。
例如,假設一家導彈發射公司在日本、德國和美國德克薩斯州休斯敦設有辦事處。所有辦公室的員工都希望在發布期間參加小組電話會議。我們將指定啟動時刻,與 UTC 的偏移量為零。為此,我們使用Instant類。下面Z字串中的 表示偏移量為零, 00:00,并且發音為“Zulu”。
Instant launch = Instant.parse( "2022-05-23T05:31:23.000Z" ) ;
讓我們告訴每個辦公室他們加入電話會議的截止日期。
ZonedDateTime tokyo = launch.atZone( ZoneId.of( "Asia/Tokyo" ) ) ;
ZonedDateTime berlin = launch.atZone( ZoneId.of( "Europe/Berlin" ) ) ;
ZonedDateTime chicago = launch.atZone( ZoneId.of( "America/Chicago" ) ) ; // Houston time zone is "America/Chicago".
我們有四個日期時間物件,它們都代表同一個同時發生的時刻。
至于您的資料庫會話和后端服務器的當前默認時區,這些應該與您的編程無關。作為程式員,這些是你無法控制的,并且可能會發生變化。因此,請始終指定您想要/預期的時區。
至于你的問題的其余部分,我無法辨別一個具體的問題。所以我所能做的就是評論你的例子。
當地時間
公司有一項政策,即午餐時間在其全球每個工廠的下午 12:30 開始。
是的,LocalTime對于代表世界上任何地方的時間是正確的。
讓我們問一下印度德里的工廠是否已經開始午餐。
LocalTime lunchStart = LocalTime.of( 12 , 30 ) ;
ZoneId zKolkata = ZoneId.of( "Asia/Kolkata" ) ;
ZonedDateTime nowKolkata = ZonedDateTime.now( zKolkata ) ;
boolean isBeforeLunchKolkata = nowKolkata.toLocalTime().isBefore( lunchStart ) ;
實際上,我們在那里有一個微妙的錯誤。在某些區域的某些日期,時間12:30可能不存在。例如,假設該日期的時區具有夏令時 (DST) 轉換,即中午一小時到下午 1 點“提前”。在那種情況下,不會有 12:30。時間 13:30 將是“新的 12:30”。
為了修復這個錯誤,讓我們調整從nowKolkata使用一天中的不同時間。在此調整期間,java.time 會考慮任何例外,例如 DST,并進行處理。
請注意,這種移動到新的一天中的時間會導致新的單獨的ZonedDateTime. java.time類使用不可變物件。
ZonedDateTime lunchTodayKolkata = nowKolkata.with( lunchStart ) ;
現在我們應該重寫午餐是否已經開始的測驗。
boolean isBeforeLunchKolkata = nowKolkata.isBefore( lunchTodayKolkata ) ;
讓我們問一下距離午餐開始還有多長時間。Durationclass 表示未附加到時間線的時間跨度,以小時-分鐘-秒為單位。
Duration duration = Duration.between( nowKolkata , lunchTodayKolkata ) ;
我們可以使用該持續時間作為確定午餐是否在該工廠開始的另一種方式。如果持續時間為負數,我們就知道我們已經過了午餐開始時間——這意味著我們必須及時回傳才能看到午餐開始時間。
boolean isAfterLunchKolkata = duration.isNegative() ;
boolean isBeforeLunchKolkata = ! duration.isNegative() ;
LocalDate
該類LocalDate僅表示日期,沒有時間,也沒有時區的背景關系或與 UTC 的偏移量。
請注意,對于任何特定時刻,日期在全球各地都會因區域而異。所以現在在日本東京可能是“明天”,而在美國俄亥俄州托萊多則是“昨天”。所以 aLocalDate在時間線方面本質上是模棱兩可的。
生日 - 如果知道某人當天的年齡至關重要,那么僅日期值是不夠的。只有一個約會,一個在日本東京看起來年滿 18 歲的人在美國俄亥俄州托萊多仍然是 17 歲。
如果您想準確了解某人的年齡,除了出生日期和出生時區之外,您還需要他們的出生時間。僅僅他們出生地的日期和時區不足以將他們的年齡縮小到他們 18 歲的第一刻。
美國俄亥俄州托萊多的時區是America/New_York。
LocalDate birthDate = LocalDate.of( 2000 , Month.JANUARY , 23 ) ;
LocalTime birthTime = LocalTime.of( 3 , 30 ) ;
ZoneId birthZone = ZoneId.of( "America/New_York" ) ;
ZonedDateTime birthMoment = ZonedDateTime.of( birthDate , birthTime , birthZone ) ;
要準確確定此人是否為 18 歲,請在出生時刻加上 18 歲。然后與當前時刻進行比較。
ZonedDateTime turning18 = birthMoment.plusYears( 18 ) ;
ZonedDateTime nowInBirthZone = ZonedDateTime.now( birthZone ) ;
boolean is18 = turning18.isBefore( nowInBirthZone ) ;
合同中的到期日。如果僅表示日期,日本的利益相關者將認為任務過期,而托萊多的另一利益相關者將任務視為準時。
真的。
A contract needing moment precision must state a date, a time-of-day, and a time zone.
Let's say a contract expires after the 23rd of May next year as seen in Chicago IL US.
I suggest avoiding the imprecise notion of "midnight". Focus on the first moment of a day on a certain date in a certain zone. So "after the 23rd" means the first moment of the 24th.
Some dates in some zones may start a time other than 00:00. Let java.time determine the first moment of a day.
LocalDate ld = LocalDate.of( 2023 , Month.MAY , 23 ) ;
ZoneId zChicago = ZoneId.of( "America/Chicago" ) ;
ZonedDateTime expiration = ld.plusDays( 1 ).atStartOfDay( zChicago ) ;
boolean isContractInEffect = ZonedDateTime.now( zChicago ).isBefore( expiration ) ;
LocalDateTimeChristmas starts at midnight on the 25th of December 2015.
Use LocalDateTime when you mean a date and time as seen in any locality.
Christmas starts at the first moment of the 25th of December in 2015 in every locality. So yes, use LocalDateTime.
LocalDateTime xmas2015 = LocalDateTime.of( 2015 , Month.DECEMBER , 25 , 0 , 0 , 0 , 0 ) ;
That class purposely lacks the context of a time zone or offset-from-UTC. So an object of this class is inherently ambiguous with regard to the timeline. This class cannot represent a moment.
In other words, Santa arrives in Kiribati (the most advanced time zone, at 14 hours ahead of UTC) before arriving in Japan, and arrives still later in India, still later in Europe, and so on. The red sleigh chases each new day dawning successively in zone after zone around the world all night long.
Let's determine the moment Christmas started then in Jordan.
ZoneId zAmman = ZoneId.of( "Asia/Amman" ) ;
ZonedDateTime xmasAmman = xmas2015.atZone( zAmman) ;
Adjust that moment to the time zone for Denver Colorado US.
ZoneId zDenver = ZoneId.of( "America/Denver" ) ;
ZonedDateTime xmasStartingInAmmanAsSeenInDenver = xmasAmman.withZoneSameInstant( zDenver ) ;
如果你審問那個xmasStartingInAmmanAsSeenInDenver物體,你會發現當圣誕節在約旦開始時,丹佛的日期仍然是 24 日,也就是圣誕節的前一天。圣誕節還要再過幾個小時才能到達科羅拉多州。
預約——在“本地”牙醫預約的意義上
在不考慮時區規則的任何更改的情況下,旨在堅持其指定時間的未來約會必須作為三個單獨的部分進行跟蹤:
- 日期與時間
- 解釋該日期和時間的時區。
- 持續時間,約會將持續多長時間。
讓我們預約明年 1 月 23 日下午 3 點在新西蘭的約會。
LocalDateTime ldt = LocalDateTime.of( 2023 , 1 , 23 , 15 , 0 , 0 , 0 ) ;
ZoneId z = ZoneId.of( "Pacific/Auckland" ) ;
Duration d = Duration.ofHours( 1 ) ;
這三個專案中的每一個都必須存盤在您的資料庫中。
- 第一個可以存盤在類似于 SQL 標準型別的型別的列中
TIMESTAMP WITHOUT TIME ZONE。注意“沒有”,而不是“有”。 - 時區可以存盤為文本,其標準化名稱格式為
Continent/Region. 切勿使用 2-4 個字母的偽區域,例如IST,CST等。 - 我會以標準ISO 8601格式存盤持續時間,
PnYnMnDTnHnMnS.P標志著開始,將T兩個部分分開。所以1小時是PT1H。
當您需要創建時間表時,您必須確定一個時刻。為此,請組合您的部分。
LocalDateTime ldt = myResultSet.getObject( … , LocalDateTime.class ) ;
ZoneId z = ZoneId.of( myResultSet.getString( … ) ) ;
Duration d = Duration.parse( myResultSet.getString( … ) ) ;
ZonedDateTime start = ldt.atZone( z ) ;
ZonedDateTime end = start.plus( d ) ;
我和其他人已經多次解釋了這一點,因此請搜索以了解更多資訊。關鍵是,世界各地的政客都表現出頻繁改變其管轄范圍內的時區規則的嗜好。他們這樣做是在幾乎沒有預先警告的情況下做到的。因此,下午 3 點約會的那一刻可能會比您現在預期的早一個小時,或者半小時后,或者誰知道呢。期望您感興趣的時區保持穩定將注定您的軟體最終會失敗。你可能認為我偏執。讓我們回顧一下……當您的預訂應用程式中的所有客戶都在錯誤的時間到達時。
立即的
某些關鍵功能必須在每天凌晨 1:30 開始(LocalDateTime 和 ZonedDateTime 在這種情況下是錯誤的,因為例如在從/到標準時間到夏令時的切換程序中,功能運行兩次/根本不運行)。
不,不正確,不是Instant。如果您想在每天凌晨 1:30 運行任務,則需要LocalTime結合使用ZoneId. 這在概念上類似于上面直接討論的任命。
如果我們希望芝加哥辦公室的服務器在凌晨運行該任務,我們將定義日期時間和區域。
ZoneId z = ZoneId.of( "America/Chicago" ) ;
LocalTime targetTime = LocalTime.of( 1 , 30 ) ;
要確定下一次運行,請捕獲當前時刻。
ZonedDateTime now = ZonedDateTime.now( z ) ;
看看我們是否在目標時間之前,然后計算等待時間。我們使用“not after”作為“if before or equal to”的更簡潔的表達方式。想想當前時刻是否恰好是上午1:30:00.000000000。
if( ! now.toLocalTime().isAfter( targetTime ) ) { // If before or equal to the target time.
Duration d = Duration.between( now , now.with( targetTime ) ) ;
…
}
如果今天已經過了目標時間,則添加一天。然后計算直到下一次運行的時間。
else { // Else, after target time. Move to next day.
LocalDate tomorrow = now.toLocalDate().plusDays( 1 ) ;
ZonedDateTime nextRun = ZonedDateTime.of( tomorrow , targetTime , z ) ;
Duration d = Duration.between( now , nextRun ) ;
…
}
ZonedDateTime.of如果您指定的時間在該區域的該日期不存在,該方法會自動調整。
如果您正在使用執行器服務在同一本地時間運行您的任務,那么您不能使用ScheduledExecutorService方法scheduleAtFixedRate或scheduleWithFixedDelay. 使用schedule具有單個延遲的方法。
將對該執行程式服務的參考傳遞給任務的建構式 your Runnable/ Callable。然后以這樣的方式撰寫您的任務,即它所做的最后一件事以新計算的延遲將自己提交給執行程式服務。在大多數情況下,延遲將是 24 小時。但在夏令時 (DST) 轉換等例外日子,延遲可能是 23 小時或 25 小時。因此,每個任務運行都會安排下一個任務運行。
uj5u.com熱心網友回復:
兩者都不。對于給定位置的約會(無論是視頻通話會議還是只是理發師的約會),您需要ZonedDateTime.
講故事的時間!
假設你計劃在 2024 年夏天的某個地方去阿姆斯特丹,出于某種奇怪的原因,你想確保自己看起來很棒,所以你有點發瘋了,決定在 5 月 14:00 預約理發店2024 年 2 月 2 日(那是星期三),在阿姆斯特丹的某個特定理發店。
我們可以精確計算出在您的理發店預約結束之前需要經過多少秒:會有某個時刻,您可以告訴地球上的任何人等待確切的秒數,然后拍手。他們什么時候拍手?您預約的確切時間。當然,如果有人要在加利福尼亞鼓掌,那將是加利福尼亞的一大早,但你可以這樣做。這就是重點——你談論的是一個特定的時刻,這意味著它會是一天中的不同時間,具體取決于地球上的位置。
但是,這甚至不是假設:歐盟不久前決定歐盟應該停止夏令時。歐盟目前正忙于其他一些事情,因此該指令的執行仍在討論中,但已經決定 - 所以現在的辯論是關于歐盟何時停止夏令時以及哪個時區是什么過去被稱為“中歐時間”,被絕大多數歐盟國家(這是一個非常大的時區)使用,需要成為。
值得注意的是,歐盟尚未就此做出決定。事實上,每個國家都可以自由選擇他們喜歡的任何東西。因此,很有可能,例如荷蘭,它位于這個非常大的區域的西部邊緣,您的理發店在那里預約了它,將決定堅持目前的“冬季時間”,因為德國很可能會這樣做,并且兩國公民都已經習慣了在兩國同時進行。但是,堅持夏季時間也很合理。我們根本不知道。
有一天,在議會中,一把錘子會落下。當錘子落下時,正是荷蘭時區定義正式更改的時刻,假設他們選擇堅持冬季,就在錘子落下的那一刻,您的理發師預約僅移動了 1 小時。時間仍然是 2024 年 5 月 2 日 14:00,在阿姆斯特丹——但在錘子落在海牙的荷蘭議會的那一刻,距離你的約會的秒數改變了 3600 秒。如果您將這一時刻及時存盤在 UTC 中,那么現在將是錯誤的。如果您以剩余秒數或 UTC 紀元 1970 年午夜午夜的秒數及時存盤這一時刻,那么現在將是錯誤的。所以不要以這些方式存盤約會。
這就解釋了 Instant timezone 和 ZonedDateTime 物件之間的實際差異。
ZonedDateTime 物件表示“我Europe/Amsterdam在 2024 年 5 月 2 日 14:00 有一個約會”的概念。
鑒于這正是它所代表的,如果您的 tzdata 檔案已更新以反映錘子落下,那沒關系。現在仍然是 2024 年 5 月 2 日 14:00。就“距離約會還有多少秒”而言,當您的 JVM 使用新的 tz 資訊更新時,該物件表示的時間點將移動 1 小時。
Instant是不同的。Instant 以“距離該時刻還有多少秒”表示一個確切的時刻。將該約會存盤為 的Instant行為將涉及計算直到它發生的秒數,然后存盤該。而且,正如我們剛剛確定的那樣,這是不正確的這意味著你會錯過約會。這不是約會的作業方式(它的作業方式是年、月、日、小時、分鐘和秒,以及可能隱含的時區 - 任何人類使用的議程都不是基于“你的一天是什么樣子”的想法從現在開始的 134823958150154 秒?') - 您應該始終根據資料型別(和存盤資料)的作業方式使用資料型別(和存盤資料),而不是對其進行某些轉換,因為如果您存盤轉換,您將“編碼”您對它的依賴如果您的轉換不再有效,您的存盤和東西就會出錯。這就是為什么將其存盤為Instant此處的錯誤。
這個故事還解釋了為什么存盤時區資訊的唯一非愚蠢方式是這樣的Europe/Amsterdam——因為像“CET”(中歐時間)這樣的東西可能在 2 年后甚至不存在。絕對不能保證今天處于 CET 的眾多國家中的每一個國家都會在歐盟指令生效時選擇同一個區域,無論何時。因此,如果您將預約存盤為“在 CET 區域中”,那么您所知道的,到 2024 年,您現在不知道您的理發店的預約實際上是什么時候。不是那么有用。
協調見面的時間,沒有地點的背景
每隔一段時間,您就想協調一個見面的時間,而不是一個見面的地方——例如虛擬會議。通常這些仍然是“本地化的”(某人被視為主持人,他們計劃會議時間為 17:30,所有參與者都存盤該ZonedDateTime結構,他們的議程系統將告訴他們何時在適當的時間,無論他們在地球上的什么地方)。所以我就這么做。
但鑒于會議是虛擬的,“隱含時區”不再一定是真實的。在這種情況下,可以將事物存盤為Instant(并且ZonedDateTimeUTC 的區域與 的概念非常接近,Instant基本上沒有區別),在這種情況下,例如荷蘭的某個人會看到他們的約會移動錘子落下的那一刻。假設他們做對了,并向他們的日歷工具表明約會在 UTC 區域內。
你可能想要這個。但更常見的是使用“主機區”路線。(沒有太大區別;日歷工具需要支持使用時區保存約會的概念(并且不要將其轉換為本地區域中的正確時間,因為如果主機區域或目標區域決定更改他們的 tz 資料,這種情況一直都在發生 - 一旦日歷工具支持這一點,您就可以選擇使用 UTC 作為此類虛擬會議的主機區域)。
資料庫
您或多或少正確地得出結論,有 3 個根本不同的,因此完全不相容的日期和時間概念;您不能在不丟失任何東西的情況下將一個轉換為另一個。這些都是:
- LocalDate(以及 LocalTime 和 LocalDateTime):如果您想在 08:00 醒來,并且即使您飛往不同的區域,您也希望該鬧鐘在本地繼續在 08:00 喚醒您,這就是您想要的。
- ZonedDateTime:用于約會。
- 即時:記錄從現在或過去 X 秒后發生或將要發生的事情;扣籃,例如日志訊息上的時間戳,顯然是用于太空東西的扣籃(太陽耀斑將發生在X-當錘子在荷蘭議會落下時,報紙計算出太陽耀斑的確切日期,小時, 分鐘, 更早列印, 現在是錯誤的, 當然, 他們必須列印一個更正。 )。
不幸的是,大多數語言(包括 java 的過時java.util.Date和java.util.Calendar方法)都把這搞砸了,沒有意識到時間就是那么復雜,而且上述 3 個概念不能在不丟失重要細微差別的情況下轉換。
因此,DB 有時也玩得又快又松。更重要的是,JDBC 通常是 Java 應用程式用來與 DB 對話的“粘合劑”,它有一個涉及型別java.sql.Date和的 API,這兩種型別java.sql.Timestamp都基于j.u.Date(它們對其進行了擴展)和 juDate,盡管名稱如此,相當于Instant- 它存盤一個時刻,首先不存盤時區,如果時區上升并更改您的定義,它將中斷。
因此,您必須使用實際的 JDBC 呼叫來輸入和輸出正確的資料型別是很棘手的。理論正確答案直接是LDT和ZDT:
ResultSet rs = somePreparedStatement.query();
/* WRONG */ {
Date x = rs.getDate(1);
ZonedDateTime y = convertToZDT(x);
}
/* RIGHT */ {
ZonedDateTime y = rs.getObject(1, ZonedDateTime.class);
}
但是,許多 JDBC 驅動程式不支持這一點。非常痛苦。類似地,要將準備好的陳述句中的問號替換為日期,正確的移動是ps.setObject(1, someZdtInstance),而不是j.u.Date實體甚至java.sql.Date實體。他們是否支持,oof。你必須檢查 - 許多人沒有。
我建議您查看資料庫檔案,了解它們支持的日期型別以及它實際存盤它們的方式。如果聽起來不錯(也就是說,它將實際的年、月、日、小時、分鐘、秒和完整的時區名稱存盤在一個列中),請使用它。如果沒有任何資料型別支持,請自己做 7 列。
假設您找到了可以完成這項作業的資料型別,請確定是否.getObject(x, ZDT.class)有效,并且ps.setObject(x, zdtInstance). 希望如此。如果是這樣,這就是你的答案。如果沒有,請四處尋找解決方法:撰寫轉換代碼并檢查到達的內容 - 畢竟,如果資料庫正在存盤實際的 y/m/d/h/m/s/zone 資訊,而您給它一個沒有該資訊的 juTimestamp 物件,那么資料庫將不得不再次將其轉換回 ymdhmsz 資訊。假設效果很好,那么這種解決方法不會對您不利。即使約會是在阿姆斯特丹,并且從現在到那時,錘子都會落下。
轉載請註明出處,本文鏈接:https://www.uj5u.com/shujuku/474377.html
