我应该在 Java 实体中使用 Instant、DateTime 或 LocalDateTime?

3 java timestamp date spring-data-jpa spring-boot

在我的 Java(使用 Spring Boot 和 Spring Data JPA)应用程序中,我通常使用 Instant。另一方面,我想对时间值使用最合适的数据类型。

您能帮我解释一下这些问题吗?在以下情况下,我应该选择哪种数据类型来保存日期和时间:

1.将时间精确地保留为时间戳(我不确定Instant是否是最佳选择)?

2.对于正常情况,当我只需要日期和时间时(据我所知,旧库已经过时,但不确定我应该使用哪个库)。

我还考虑了时区,但不确定使用 LocalDateTime 与 UTC 是否可以解决我的问题。

任何帮助,将不胜感激。

rzw*_*oot 10

假设我们需要涵盖整个日期和时间问题。如果您没有某种担忧,那么要么将各种类型折叠为“那么它们是可以互换的”,要么只是意味着您不需要使用 API 的特定部分。关键是,您需要了解这些类型代表什么,一旦了解了这一点,您就知道要应用哪一种。因为即使各种不同的java.time类型在技术上都可以实现您想要的功能,但如果您使用的类型代表了您希望它们实现的功能,则代码会更加灵活并且更易于阅读。出于同样的原因,String[] student = new String[] {"Joe McPringle", "56"};这也许是机械地表示学生姓名和年龄的一种方式,但如果您编写 aclass Student { String name; int age; }并使用它,事情就会简单得多。

\n

本地闹钟

\n

想象一下您想在早上 07:00 起床。不是因为你有约会,你只是喜欢早起。

\n

因此,您将闹钟设置为早上 07:00,然后去睡觉,闹钟会在 7 点准时响起。到目前为止,一切顺利。然而,您随后会跳上飞机从阿姆斯特丹飞往纽约。(纽约比纽约早 6 小时)。然后你再去睡觉。闹钟应该在晚上 01:00 响,还是早上 07:00 响?

\n

两个答案都是正确的。问题是,如何“存储”该警报,要回答这个问题,您需要弄清楚警报试图代表什么。

\n

如果意图是“07:00,无论闹钟响起时我在哪里”,正确的数据存储机制是java.time.LocalDateTime,它以人类的方式存储时间(年、月、日、小时、分钟和秒),而不是计算机术语(我们稍后会讲到),并且根本不包括时区。如果闹钟应该每天响起,那么您也不希望这样,因为 LDT 存储日期时间,因此您可以使用该名称LocalTime

\n

那是因为您想要存储“闹钟应该在 7 点钟响起”的概念,仅此而已。你无意说:“当阿姆斯特丹的人们都同意现在是 07:00 时,闹钟就应该响起”,你也无意说:“当宇宙到达这个确切的时刻时,请敲响警报。”警报”。您的意图是说:“当 07:00 时,无论您现在身在何处,请拉响警报”,因此请存储,它是LocalTime.

\n

同样的原则也适用于LocalDate:它存储一个年/月/日元组,没有位置的概念。

\n

这确实得出了一些可能不太可靠的结论:给定一个LocalDateTime对象,不可能询问LDT 到达需要多长时间。也不可能将任何特定时刻与 LDT 进行比较,因为这些东西都是苹果和橙子。“2023 年 2 月 18 日早上 7 点整”这个概念并不是一个单一的时间。毕竟,在纽约,这个“时刻”比在阿姆斯特丹早了整整 6 个小时。您只能比较 2 个 LocalDateTime。

\n

相反,您必须首先通过询问 java.time API 将 LDT 转换为其他类型之一(ZonedDateTime 甚至 Instant),将其“放置”在某个地方:好的,我希望这个特定的 LDT 位于某个时区。

\n

因此,如果您正在编写闹钟应用程序,则必须获取存储的闹钟(一个LocalTime对象),将其转换为即时(这就是“现在是什么时间,即”的本质System.currentTimeMillis()),方法是说:本地时间,当前本地时区当天的即时,然后比较这两个结果。

\n

人工预约

\n

想象一下,就在飞往纽约之前,您预约了当地(阿姆斯特丹)的理发师。他们的议程有点忙,所以约会定在 2025 年 6 月 20 日 11:00。

\n

如果你在纽约待了几年,你的日历提醒你一小时后与理发师预约的正确时间肯定不是纽约2025 年 6 月 20 日 10:00。到时候你就已经错过了约会。相反,你的手机应该在半夜 04:00 提醒你,你还有一个小时的时间去理发店(当然,从纽约出发有点棘手)。

\n

听起来我们确实可以说理发师的预约是一个特定的时刻。然而,这是不正确的。欧盟已经通过了所有成员国一致同意的立法,要求所有欧盟国家废除夏令时。然而,这项法律并没有规定最后期限,更重要的是,没有规定每个欧盟成员国需要选择的时区。因此,荷兰在某个时候更改时区。他们可能会选择坚持永久夏令时(在这种情况下,他们将永久使用 UTC+2,而目前的情况是夏季使用 UTC+2,冬季使用 UTC+1,值得注意的是,转换发生的日期与纽约不同!),或者永远保留冬令时,即 UTC+1。

\n

假设他们选择永远坚持冬季。

\n

荷兰议会大楼敲下木槌,将荷兰人三月份不再将时钟提前的那一天,就是你的约会推迟一小时的那一天。毕竟,您的理发师不会进入他们的预约簿并将所有预约推迟一个小时。不,您的预约将保留在 2025 年 6 月 20 日 11:00。如果你有一个正在倒计时直到预约理发师的时钟,那么当木槌落下时,它应该跳动 3600 秒。

\n

这掩盖了这一点:理发师的任命确实不是一个单一的时刻。这是人类/政治协议,阿姆斯特丹普遍同意当前时间为 2025 年 6 月 20 日,11:00 \xe2\x80\x93,谁知道那一刻实际上会发生;这取决于政治选择

\n

因此,您无法通过存储即时时间来“解决”这个问题,并且它显示了“即时时间”和“特定时区的年/月/日时:分:秒”概念如何不能完全互换

\n

此概念的正确数据类型是ZonedDateTime. 这表示人类术语中的日期时间:年/月/日时:秒:分钟时区。它不会通过在纪元或类似的时间中存储某个时刻来走捷径。如果木槌落下并且您的 JDK 更新了其时区定义,则询问“距离我的约会还有多少秒”将正确地移动 3600 秒,这正是您想要的。

\n

因为这是用于约会的,并且只存储约会时间而不存储日期是没有意义的,所以不存在 aZonedDate或 a之类的东西ZonedTime。与第一个有 3 种口味(LocalDateTimeLocalDateLocalTime)的东西不同,只有ZonedDateTime.

\n

宇宙/记录时间

\n

想象一下,您正在编写一个计算机系统来记录发生的事件。

\n

该事件自然有一个与之关联的时间戳。事实证明,由于严重的政治动荡,该国的法律决定该国的时与事件发生时您所想象的不同。应用与理发师案例相同的逻辑(当木槌落下时,实际时间跳跃 3600 秒)是不正确的。时间戳代表事情发生的时刻,而不是账本中的约会。它不应该跳到3600。

\n

时区在这里确实没有任何意义。存储日志事件的“时间戳”的目的是让您知道它何时发生,无论它发生在哪里(或者如果发生了,这从根本上来说是一个单独的概念)。

\n

正确的数据类型是java.time.Instant. 瞬间根本不知道时区,也不是人类的概念。这是“计算机时间”——自商定的纪元(午夜、UTC、1970、新年)以来存储为毫秒,这里不需要时区信息或理智的时区信息。当然,不存在仅时间或仅日期的变体,这个东西甚至不真正知道“日期”是什么 - 一些花哨的人类概念,计算机时间根本不关心。

\n

转换

\n

您可以轻松地从 a 转到ZonedDateTimean Instant。有一个无参数方法可以做到这一点。但请注意:

\n
    \n
  1. 创建一个 ZonedDateTime。
  2. \n
  3. 将其存放在某个地方。
  4. \n
  5. 将其转换为 Instant,也将其存储。
  6. \n
  7. 更新您的 JDK 并获取新的时区信息
  8. \n
  9. 加载 ZDT。
  10. \n
  11. 第二次将其转换为 Instant。
  12. \n
  13. 比较 2 个 ZDT 和 2 个瞬间。
  14. \n
\n

导致不同的结果:两个时刻不相同,但 ZDT 相同。ZDT 代表理发师手册中的预约线(从未改变 - 2025 年 6 月 20 日,11:00),瞬间代表您应该出现的时间点,但确实发生了变化。

\n

如果您将理发师的预约存储为java.time.Instant对象,那么您的理发师预约将会迟到一个小时。这就是为什么按原样存储东西很重要。理发师的预约是ZonedDateTime. 像其他任何东西一样存储它是错误的。

\n

转换很少是真正简单的。没有一种方法可以将一种事物转换为另一种事物 - 您需要考虑这些事物代表什么,转换意味着什么,然后照着做。

\n

示例:您正在编写一个日志系统。后端部分将日志事件存储到某种数据库中,前端部分读取该数据库并向管理员用户显示日志事件以供查看。因为管理员用户是一个人,所以他们希望以他们理解的方式查看它,例如根据 UTC 的时间和日期(这是一个程序员,他们倾向于喜欢这类事情)。

\n

日志系统的存储应该存储Instant概念:纪元毫秒,并且没有时区,因为这是不相关的。

\n

前端应该将这些读取Instant(进行静默转换总是一个坏主意!) - 然后考虑如何将其呈现给用户,弄清楚用户希望这些作为本地到 UTC,因此你可以苍蝇,对于要打印到屏幕上的每个事件,将 Instant 转换为用户想要的区域中的 ZonedDateTime,然后从那里转换为 LocalDateTime,然后渲染它(因为用户可能不希望在UTC每一行上看到他们的屏幕空间有限)。

\n

将时间戳存储为 UTC ZonedDateTimes 是不正确的,将它们存储为 LocalDateTimes 则更错误,这是通过在事件发生时询问 UTC 中的当前 LocalDT 然后存储它而派生的。从机械上讲,所有这些事情都可以工作,但数据类型都是错误的。这会让事情变得复杂。想象一下用户实际上想要查看欧洲/阿姆斯特丹时间的日志事件。

\n

关于时区的说明

\n

世界比少数时区更复杂。例如,几乎所有欧洲大陆目前都是“CET”(中欧时间),但有些人认为指的是欧洲冬季时间(UTC+1),有些东西指的是中欧当前的状态:UTC+1冬季,夏季 UTC+2。(还有 CEST,中欧夏令时间,这意味着 UTC+2 并且不含糊)。当欧盟国家开始应用新法律取消夏令时时,例如位于 CET 区域西边缘的荷兰可能会选择与东边缘的波兰不同的时间。因此,“整个中欧”过于宽泛。三字母缩写词也绝不是唯一的。许多国家都使用“EST”来表示“东部标准时间”,例如不仅仅是美国东部。

\n

因此,表示时区名称的唯一正确方法是使用类似Europe/Amsterdam或 的字符串Asia/Singapore。如果你需要像09:00 PST美国西海岸的居民一样渲染这些,那就是渲染问题,所以,写一个渲染方法,变成America/Los_AngelesPST这是本地化问题,与时间无关。

\n


Bas*_*que 7

rzwitserloot 的答案是正确且明智的。另外,这里对各种类型进行了总结。欲了解更多信息,请参阅我的答案对类似问题的回答。

\n
\n
    \n
  1. 要将时间精确地保留为时间戳(我不确定 Instant 是否是最佳选择)?
  2. \n
\n
\n

如果您想跟踪某个时刻,即时间轴上的特定点:

\n
    \n
  • Instant
    与 UTC 偏移为零时-分-秒的时刻。这个类是java.time的基本构建块
  • \n
  • OffsetDateTime
    以特定偏移量看到的时刻,即在 UTC 时间子午线之前或之后的一些小时、分钟、秒数。
  • \n
  • ZonedDateTime
    在特定时区看到的时刻。时区是特定地区人民使用的偏移量的过去、现在和未来变化的命名历史,由他们的政治家决定。
  • \n
\n

如果您只想跟踪日期和时间,而不需要偏移量或时区的上下文,请使用LocalDateTime。这个类并不代表某个时刻,也不是时间轴上的一个点。

\n
\n
    \n
  1. 对于正常情况,当我只需要日期和时间时
  2. \n
\n
\n

如果您绝对确定只需要带有当天时间的日期,但不需要偏移量或时区的上下文,请使用LocalDateTime

\n
\n

将 LocalDateTime 与 UTC 结合使用

\n
\n

这是矛盾的,也是没有道理的。类LocalDateTime没有 UTC 的概念,也没有任何与 UTC 或时区的偏移量的概念。

\n
\n

Spring数据JPA

\n
\n

JDBC 4.2+ 规范将 SQL 标准数据类型映射Java类。

\n
    \n
  • TIMESTAMP WITH TIME ZONEJava 中的列映射OffsetDateTime
  • \n
  • TIMESTAMP WITHOUT TIME ZONEJava 中的列映射LocalDateTime
  • \n
  • DATE列映射到LocalDate.
  • \n
  • TIME WITHOUT TIME ZONE列映射到LocalTime.
  • \n
\n

SQL标准也提到了TIME WITH TIME ZONE,但是这种类型是没有意义的(想想看!)。据我所知,SQL 委员会从未解释过他们的想法。如果必须使用此类型,Java 会定义ZoneOffset要匹配的类。

\n

请注意,JDBC不会任何 SQL 类型映射到Instantnor ZonedDateTime。您可以轻松地与映射类型进行转换OffsetDateTime

\n
Instant instant = myOffsetDateTime.toInstant() ;\nOffsetDateTime myOffsetDateTime = instant.atOffset( ZoneOffset.UTC ) ;\n
Run Code Online (Sandbox Code Playgroud)\n

\xe2\x80\xa6 和:

\n
ZonedDateTime zdt = myOffsetDateTime.atZoneSameInstant( myZoneId ) ;\nOffsetDateTime odt = zdt.toOffsetDateTime() ;  // The offset in use at that moment in that zone.\nOffsetDateTime odt = zdt.toInstant().atOffset( ZoneOffset.UTC ) ;  // Offset of zero hours-minutes-seconds from UTC.\n
Run Code Online (Sandbox Code Playgroud)\n
\n

我也考虑时区

\n
\n

该类TimeZone是可怕的遗留日期时间类的一部分,这些类在几年前被现代java.time类取代。替换为ZoneIdZoneOffset

\n