提问者:小点点

我应该在Java实体中使用Instant或DateTime或LocalDateTime吗?[关闭]


想改进这个问题吗?更新问题,以便通过编辑这篇文章用事实和引用来回答。

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

你能澄清一下这些问题吗?当出现以下情况时,我应该选择什么数据类型来保存日期和时间:

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

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

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

任何帮助将不胜感激。


共2个答案

匿名用户

让我们假设我们需要涵盖日期和时间关注点的全部范围。如果你没有某个关注点,那要么将各种类型折叠成“那么它们是可互换的”,要么简单地意味着你不需要使用API的某个部分。关键是,你需要理解这些类型代表什么,一旦你知道了,你就知道该应用哪一个。因为即使各种不同的java. time类型在技术上都能满足你的需求,如果你使用的类型代表了你想要的东西,代码也会更加灵活,阅读起来也简单得多。出于同样的原因String[]学生=new String[]{"Joe McPringle","56"};也许是一种机械地表示学生姓名和年龄的方法,但是如果你写一个类学生{String name; int age;}并使用它,事情会简单得多。

想象一下你想在早上07:00醒来。不是因为你有约会,你只是喜欢做一个相当早起的人。

所以你把闹钟设置在早上07:00,睡觉,闹钟很快在7点响起。到目前为止,一切都很好。然而,你随后跳上飞机,从阿姆斯特丹飞往纽约。(纽约早6个小时)。然后你又去睡觉了。闹钟应该在晚上01:00还是早上07:00响起?

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

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

那是因为你想储存“闹钟应该在7点响起”的概念,仅此而已。你无意说:“当阿姆斯特丹的人们同意现在是07:00时,闹钟应该响起”,你也无意说:“当宇宙在这个确切的时刻到来时,拉响警报”。你的意图是说:“当它是07:00时——无论你现在在哪里,拉响警报”,所以储存起来,这是一个LocalTime

同样的原则也适用于LocalDate:它存储一个年/月/日元组,不知道在哪里。

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

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

因此,如果您正在编写警报应用程序,则必须将存储的警报(LocalTime对象)转换为Instant(这就是“现在是什么时间”的性质,即System. CurrentTimeMillis()的工作原理),方法是说:在当前本地时区的当前日期的LocalTime作为一个瞬间,然后比较这两个结果。

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

如果你在纽约呆了几年,你的日历提醒你一小时后和你的理发店有个约会的正确时间肯定不是纽约2025年6月20日10:00。那时你已经错过了约会。相反,你的手机应该告诉你,你还有一个小时在半夜4:00去理发店(当然,从纽约来有点棘手)。

听起来我们可以说理发师的任命是一个特定的时间点。然而,这是不正确的。欧盟已经通过了所有成员国都同意的立法,所有欧盟国家都应废除夏令时。然而,这项法律没有规定最后期限,最重要的是,没有提供每个欧盟成员国需要选择的时区。因此,荷兰将在某个时候改变时区。他们可能会选择坚持永久的夏季时间(在这种情况下,他们将永久处于UTC2,而不是他们目前的情况,夏季UTC2,冬季UTC1,值得注意的是,切换发生的日期与纽约不同!),或者永远停留在冬季,即UTC1。

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

荷兰议会大厦里的木槌敲响的那一天,就是你的预约时间提前一小时的那一天。毕竟,你的理发师不会进入他们的预约簿,把所有的预约时间都提前一小时。不,你的预约时间将保持在2025年6月20日的11:00。如果你的理发师预约时间倒计时,当木槌落下时,它应该会跳3600秒。

这掩盖了一点:理发师的任命真的不是一个单一的时刻。这是一个人/政治协议,即你的任命是阿姆斯特丹普遍同意的时间,目前是2025年6月20日,11:00——谁知道那一刻什么时候会真正发生;这取决于政治选择。

因此,您不能通过存储时间上的瞬间来“解决”这个问题,它显示了“时间上的瞬间”和“特定时区的年/月/日小时:分钟:秒”的概念是如何不可互换的。

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

因为这是用于约会的,只存储约会时间而不存储日期是没有意义的,所以没有ZonedDateZonedTime这样的东西。与第一个有三种风格(LocalDateTimeLocalDateLocalTime)的东西不同,只有ZonedDateTime

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

自然,这一事件有一个与之相关的时间戳。事实证明,由于严重的政治动荡,这片土地的法律决定,追溯过去,这个国家所处的时区与你认为事件发生时的时区不同。应用与理发师案例相同的逻辑(当木槌落下时,实际时间点跳了3600秒)是不正确的。时间戳代表的是事情发生的时间点,而不是账本上的约会。它不应该跳3600秒。

时区在这里真的没有意义。为日志事件存储“时间戳”的目的是让你知道它是什么时候发生的,它发生在哪里并不重要(或者如果发生了,那基本上是一个单独的概念)。

正确的数据类型是java. time.Instant。即时甚至根本不知道时区,也不是人类的概念。这是“计算机时间”——从一个约定的时代(午夜、UTC、1970年、新年)开始存储为千分之一,这里没有时区信息是必要的或理智的。自然没有只有时间或只有日期的变体,这个东西甚至不知道“日期”是什么——一些幻想的人类概念,计算机时间一点也不关心。

您可以轻松地从ZonedDateTime转到Instant。有一个no-args方法可以做到这一点。但请注意:

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

结果不同:两个瞬间不会相同,但ZDT是相同的。ZDT代表理发师账本上的预约线(从未改变——2025年6月20日,11:00),瞬间代表你应该出现的时间,它确实改变了。

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

转换很少是真正简单的。没有一种方法可以将一件事转换为另一件事——你需要考虑这些事情代表什么,转换意味着什么,然后效仿。

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

日志系统的存储应该存储Instant概念:Epoch milis,并且没有时区,因为这无关紧要。

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

将时间戳存储为UTCZonedDateTimes是不正确的,更错误的是将它们存储为LocalDateTimes,方法是在事件发生时要求当前LocalDTUTC,然后将其存储。从机械上讲,所有这些东西都可以工作,但数据类型都是错误的。这将使事情复杂化。想象一下,用户实际上希望在欧洲/阿姆斯特丹时间看到日志事件。

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

因此,表示时区名称的唯一正确方法是使用像Europe/AmsterdamAsia/Singicas这样的字符串。如果您需要将这些渲染为USA西海岸居民的09:00 PST,这是一个渲染问题,因此,编写一个将America/Los_Angeles转换为PST的渲染方法,这是一个本地化问题,与时间无关。

匿名用户

rzwitserloot的答案是正确和明智的。此外,这里是各种类型的摘要。有关更多信息,请参阅我对类似问题的回答。

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

如果你想跟踪一个时刻,时间轴上的一个特定点:

  • Instant
    从零小时-分钟-秒的UTC偏移量看到的时刻。这个类是java. time框架的基本构建块。
  • OffsetDateTime
    以特定偏移量看到的时刻,在UTC颞子午线之前或之后数小时-分钟-秒。
  • ZonedDateTime
    在特定时区中看到的时刻。时区是由特定地区人民使用的偏移量的命名历史,由他们的政治家决定。

如果您只想跟踪日期和一天中的时间,而不包含偏移量或时区的上下文,请使用LocalDateTime。此类不代表时刻,也不代表时间轴上的点。

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

使用LocalDateTime与UTC

LocalDateTime类没有UTC的概念,也没有UTC或时区的概念。

Spring数据JPA

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

  • TIMESTAMP与TIME ZONE列映射到Java中的OffsetDateTime。
  • TIMESTAMP WITHOUT TIME ZONE列映射到Java中的LocalDateTime。
  • DATE列映射到LocalDate
  • TIME WITHOUT TIME ZONE列映射到LocalTime

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

请注意,JDBC不会将任何SQL类型映射到InstantZonedDateTime。您可以轻松地转换为映射类型OffsetDateTime

Instant instant = myOffsetDateTime.toInstant() ;
OffsetDateTime myOffsetDateTime = instant.atOffset( ZoneOffset.UTC ) ;

…和:

ZonedDateTime zdt = myOffsetDateTime.atZoneSameInstant( myZoneId ) ;
OffsetDateTime odt = zdt.toOffsetDateTime() ;  // The offset in use at that moment in that zone.
OffsetDateTime odt = zdt.toInstant().atOffset( ZoneOffset.UTC ) ;  // Offset of zero hours-minutes-seconds from UTC.

我也认为时区

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