LocalDateTime到ZonedDateTime

我有Java 8 Spring网络应用程序,它将支持多个区域。 我需要为客户位置制作日历活动。 因此,假设我的网站和Postgres服务器托管在MST时区(但我想如果我们去云端,它可能就在任何地方)。 但客户是在美国东部时间。 所以,按照我读到的一些最佳实践,我想我会以UTC格式存储所有日期时间。 数据库中的所有日期时间字段都声明为TIMESTAMP。

所以这是我如何采用LocalDateTime并转换为UTC:

ZonedDateTime startZonedDT = ZonedDateTime.ofLocal(dto.getStartDateTime(), ZoneOffset.UTC, null); //appointment startDateTime is a LocalDateTime appointment.setStartDateTime( startZonedDT.toLocalDateTime() ); 

现在,例如,当搜索日期时,我必须从请求的日期时间转换为UTC,获取结果并转换为用户时区(存储在数据库中):

 ZoneId userTimeZone = ZoneId.of(timeZone.getID()); ZonedDateTime startZonedDT = ZonedDateTime.ofLocal(appointment.getStartDateTime(), userTimeZone, null); dto.setStartDateTime( startZonedDT.toLocalDateTime() ); 

现在,我不确定这是正确的方法。 我也想知道是因为我从LocalDateTime到ZonedDateTime,反之亦然,我可能会失去任何时区信息。

这是我所看到的,这对我来说似乎不正确。 当我从UI收到LocalDateTime时,我得到了这个:

 2016-04-04T08:00 

ZonedDateTime =

 dateTime=2016-04-04T08:00 offset="Z" zone="Z" 

然后,当我将转换后的值分配给我的约会LocalDateTime时,我存储:

 2016-04-04T08:00 

我觉得因为我存储在LocalDateTime中我失去了转换为ZonedDateTime的时区。

我应该让我的实体(约会)使用ZonedDateTime而不是LocalDateTime,以便Postgres不会丢失这些信息吗?

—————- 编辑 —————-

在Basils出色的答案之后,我意识到我有一个不关心用户时区的奢侈 – 所有约会都是针对特定位置的,因此我可以将所有日期时间存储为UTC,然后在检索时将它们转换为位置时区。 我提出了以下后续问题

Postgres没有TIMESTAMP这样的数据类型。 Postgres有两种类型的日期加上时间: TIMESTAMP WITH TIME ZONETIMESTAMP WITHOUT TIME ZONE 。 这些类型在时区信息方面具有非常不同的行为。

  • WITH类型使用任何偏移或时区信息来调整UTC的日期时间,然后处理该偏移或时区; Postgres永远不会保存偏移/区域信息。
    • 此类型表示时刻,即时间轴上的特定点。
  • WITHOUT类型忽略可能存在的任何偏移或区域信息。
    • 这种类型并不代表片刻。 它代表了对大约26-27小时(全球时区范围)范围内潜在时刻的模糊概念。

你几乎总是想要WITH类型,正如David E. Wheeler专家所解释的那样 。 只有时间轴上的日期时间而不是固定点的模糊概念时, WITHOUT才有意义。 例如,“今年圣诞节从2016-12-25T00:00:00开始”将存储在WITHOUT因为它适用于任何时区,尚未应用于任何一个时区以获得实际时刻时间表。 如果圣诞老人的精灵跟踪Eugene Oregon US的开始时间,那么他们将使用WITH类型和包含偏移或时区的输入,例如2016-12-25T00:00:00-08:00 ,它们将保存到Postgres如2016-12-25T08:00.00Z (其中Z表示祖鲁语或UTC)。

java.time中Postgres的TIMESTAMP WITHOUT TIME ZONE相当于java.time.LocalDateTime 。 由于你的目的是使用UTC(一件好事),你应该使用LocalDateTime (一件坏事)。 这可能是您的主要困惑和麻烦。 你一直在考虑使用LocalDateTimeZonedDateTime但你不应该使用它们; 相反,你应该使用Instant (下面讨论)。

我也想知道是因为我从LocalDateTime到ZonedDateTime,反之亦然,我可能会失去任何时区信息。

的确,你是。 LocalDateTime的整个点是丢失时区信息。 所以我们很少在大多数应用程序中使用此类。 再次,圣诞节的例子。 或者另一个例子,“公司政策:我们在世界各地的所有工厂都在下午12:30吃午饭”。 这将是LocalTime ,对于特定日期, LocalDateTime 。 但是,在您应用时区来获取ZonedDateTime之前,这没有实际意义,而不是时间轴上的实际点。 午餐rest时间将在德里工厂的时间线上与杜塞尔多夫工厂不同,在底特律工厂再次不同。

LocalDateTime “Local”一词可能违反直觉,因为它意味着没有特定的地点。 当你在课程名称中读到“本地”时,请想一下“不是时刻……不是时间轴……只是关于有点日期时间的模糊概念”。

您的服务器几乎总是应在其操作系统时区中设置为UTC。 但是你的编程永远不应该依赖于这种外部性,因为系统管理员很容易改变它或者任何其他Java应用程序更改JVM中的当前默认时区。 因此,请始终指定所需/预期的时区。 (顺便说一句, Locale也是如此。)

结果:

  • 你的工作方式太难了。
  • 程序员/系统管理员必须学会“全球思考,本地化”。

在戴着你的极客安全帽的工作日期间,请考虑使用UTC。 只有在转换到你的外行人的那天结束时,你应该回想一下你所在城镇的当地时间。

您的业​​务逻辑应该关注UTC。 您的数据库存储,业务逻辑,数据交换,序列化,日志记录和您自己的想法都应该在UTC时区(顺便说一下,24小时制)完成。 向用户显示数据时,只应用特定时区。 将分区日期时间视为外部事物,而不是应用程序内部的工作部分。

在Java方面,在您的大部分业务逻辑中使用java.time.Instant (UTC时间轴上的片刻)。

 Instant now = Instant.now(); 

希望JDBC驱动程序最终会更新以直接处理像Instant这样的java.time类型。 在那之前我们必须使用java.sql类型。 旧的java.sql类具有转换为/从java.time转换的新方法。

 java.sql.TimeStamp ts = java.sql.TimeStamp.valueOf( instant ); 

现在通过setTimestamp将该java.sql.TimeStamp对象传递给PreparedStatement ,以保存到Postgres中定义为TIMESTAMP WITH TIME ZONE的列。

走向另一个方向:

 Instant instant = ts.toInstant(); 

这很容易,从Instantjava.sql.TimestampTIMESTAMP WITH TIME ZONE ,全部是UTC。 没有涉及时区。 服务器操作系统,JVM和客户端的当前默认时区都无关紧要。

要向用户显示,请应用时区。 使用正确的时区名称 ,不要使用ESTIST等3-4个字母代码。

 ZoneId zoneId = ZoneId.of( "America/Montreal" ); ZonedDateTime zdt = ZonedDateTime.ofInstant( instant , zoneId ); 

您可以根据需要调整到不同的区域。

 ZonedDateTime zdtKolkata = zdt.withZoneSameInstant( ZoneId.of( "Asia/Kolkata" ) ); 

要回到UTC中时间轴上的瞬间,您可以从ZonedDateTime提取。

 Instant instant = zdt.toInstant(); 

没有我们在哪里使用LocalDateTime

如果您确实获得了一段没有任何偏离UTC或时区的数据,例如2016-04-04T08:00 ,那么这些数据对您来说完全没用(假设我们不是在谈论圣诞节或公司午餐类型场景)上面讨论过)。 没有偏移/区域信息的日期时间就像没有指示货币的货币金额: 142.70甚至是$142.70 – 没用。 但是USD 142.70 ,或者CAD 142.70 ,或者是MXN 142.70 ……这些都是有用的。

如果您确实获得了2016-04-04T08:00值,并且您完全确定了预期的偏移/区域上下文,那么:

  1. 将该字符串解析为LocalDateTime
  2. 应用offset-from-UTC获取OffsetDateTime ,或者(更好)应用时区来获取ZonedDateTime

喜欢这段代码。

 LocalDateTime ldt = LocalDateTime.parse( "2016-04-04T08:00" ); ZoneId zoneId = ZoneId.of( "Asia/Kolkata" ); // Or "America/Montreal" etc. ZonedDateTime zdt = ldt.atZone( zoneId ); // Or atOffset( myZoneOffset ) if only an offset is known rather than a full time zone. 

你的问题确实与许多其他问题重复。 在其他问题和答案中已经多次讨论过这些问题。 我恳请您搜索和研究Stack Overflow以了解有关此主题的更多信息。

JDBC 4.2

从JDBC 4.2开始,我们可以直接与数据库交换java.time对象。 无需再次使用java.sql.Timestamp ,也不需要使用相关的类。

使用JDBC规范中定义的OffsetDateTime存储。

 myPreparedStatement.setObject( … , instant.atOffset( ZoneOffset.UTC ) ) ; // The JDBC spec requires support for `OffsetDateTime`. 

…或者如果JDBC驱动程序支持,可以直接使用Instant

 myPreparedStatement.setObject( … , instant ) ; // Your JDBC driver may or may not support `Instant` directly, as it is not required by the JDBC spec. 

检索。

 OffsetDateTime odt = myResultSet.getObject( … , OffsetDateTime.class ) ; 

关于java.time

java.time框架内置于Java 8及更高版本中。 这些类取代了麻烦的旧遗留日期时间类,如java.util.DateCalendarSimpleDateFormat

现在处于维护模式的Joda-Time项目建议迁移到java.time类。

要了解更多信息,请参阅Oracle教程 。 并搜索Stack Overflow以获取许多示例和解释。 规范是JSR 310 。

您可以直接与数据库交换java.time对象。 使用符合JDBC 4.2或更高版本的JDBC驱动程序 。 不需要字符串,不需要java.sql.*类。

从哪里获取java.time类?

  • Java SE 8Java SE 9及更高版本
    • 内置。
    • 带有捆绑实现的标准Java API的一部分。
    • Java 9增加了一些小function和修复。
  • Java SE 6Java SE 7
    • 许多java.timefunction都被反向移植到ThreeTen-Backport中的 Java 6和7。
  • Android的
    • 更高版本的Android捆绑java.time类的实现。
    • 对于早期的Android(<26), ThreeTenABP项目采用ThreeTen-Backport (如上所述)。 请参见如何使用ThreeTenABP ….

ThreeTen-Extra项目使用其他类扩展了java.time。 该项目是未来可能添加到java.time的试验场。 您可以在这里找到一些有用的课程,如IntervalYearWeekYearQuarter等。