LocalDateTime 到 ZonedDateTime

LocalDateTime to 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,然后将它们转换为位置时区检索。我做了以下跟进 question

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

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

您几乎总是想要 WITH 类型,正如专家 David E. Wheeler 所著的 explained hereWITHOUT 仅当您对日期时间有模糊的想法而不是时间轴上的固定点时才有意义。例如,“今年的圣诞节从 2016-12-25T00:00:00 开始”将存储在 WITHOUT 中,因为它适用于 any 时区,尚未已应用于任何一个时区以获得时间轴上的实际时刻。如果圣诞老人的精灵正在跟踪美国俄勒冈州尤金的开始时间,那么他们将使用 WITH 类型和一个包含偏移量或时区的输入,例如 2016-12-25T00:00:00-08:00 ,它被保存到 Postgres 中作为 2016-12-25T08:00.00Z(其中 Z 表示祖鲁语或 UTC)。

Postgres 的 TIMESTAMP WITHOUT TIME ZONE 在 java.time 中的等价物是 java.time.LocalDateTime。因为您的意图是在 UTC 中工作(好事),所以您应该 而不是 使用 LocalDateTime(坏事)。这可能是您困惑和麻烦的要点。您一直在考虑使用 LocalDateTimeZonedDateTime 但您不应该使用任何一个;相反,您应该使用 Instant(在下面讨论)。

I also wonder if because I am going from LocalDateTime to ZonedDateTime and vice versa, I might be losing any timezone info.

你确实是。 LocalDateTime 的全部意义在于丢失 时区信息。 所以我们很少在大多数应用程序中使用这个 class。同样,圣诞节的例子。或者另一个例子,“公司政策:我们在世界各地的所有工厂都在 12:30 下午吃午饭”。那将是 LocalTime,对于特定日期,LocalDateTime。但这没有实际意义,不是时间轴上的实际点,直到您应用时区以获得 ZonedDateTime。午休时间在德里工厂和杜塞尔多夫工厂的时间点上有所不同,在底特律工厂也有所不同。

LocalDateTime 中的“本地”一词可能有悖常理,因为它的意思是没有特定的 地方。当您在 class 名称中阅读“本地”时,请思考“不是片刻……不是在时间轴上……只是关于某种日期时间的模糊想法”。

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

结果:

  • 你太辛苦了
  • Programmers/sysadmins 必须学会“放眼全球,呈现本地”。

在工作期间,戴着你的极客安全帽,用 UTC 思考。只有在一天结束时切换到外行人的时间,您才应该回头想想您所在城镇的当地时间。

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

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

Instant now = Instant.now();

希望 JDBC 驱动程序最终会更新以直接处理 java.time 类型,例如 Instant。在那之前我们必须使用 java.sql 类型。旧的java.sqlclass有了新的转换方法to/fromjava.time.

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

现在将要保存的 java.sql.TimeStamp object via setTimestamp on a PreparedStatement 传递到 Postgres 中定义为 TIMESTAMP WITH TIME ZONE 的列。

往另一个方向走:

Instant instant = ts.toInstant();

所以这很简单,从 Instantjava.sql.Timestamp 再到 TIMESTAMP WITH TIME ZONE,全部采用 UTC。 不涉及时区。您的服务器 OS、您的 JVM 和您的客户端的当前默认时区都无关紧要。

要向用户展示,请应用时区。使用 proper time zone names,切勿使用 3-4 个字母的代码,例如 ESTIST.

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

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

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

要回到 Instant,UTC 时间轴上的某个时刻,您可以从 ZonedDateTime.

中提取
Instant instant = zdt.toInstant();

我们没有在什么地方使用 LocalDateTime

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

如果您确实获得了 2016-04-04T08:00 值,并且您 绝对确定 预期的 offset/zone 上下文,那么:

  1. 将该字符串解析为 LocalDateTime
  2. 应用与 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 及其相关的 classes.

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

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

…或者可能直接使用 Instant,如果你的 JDBC 驱动程序支持的话。

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 framework is built into Java 8 and later. These classes supplant the troublesome old legacy date-time classes such as java.util.Date, Calendar, & SimpleDateFormat.

要了解更多信息,请参阅 Oracle Tutorial. And search Stack Overflow for many examples and explanations. Specification is JSR 310

Joda-Time project, now in maintenance mode, advises migration to the java.time classes.

您可以直接与您的数据库交换 java.time 对象。使用 JDBC driver compliant with JDBC 4.2 或更高版本。不需要字符串,不需要 java.sql.* classes。 Hibernate 5 和 JPA 2.2 支持 java.time.

从哪里获得 java.time classes?

TLDR:

要从 LocalDateTime 转换为 ZonedDateTime 以下代码。您可以使用 .atZone( zoneId ),区域 ID 的完整列表可在 TZ database name on Wikipedia.

列中找到
LocalDateTime localDateTime = LocalDateTime.parse( "2016-04-04T08:00" );
ZoneId zoneId = ZoneId.of( "UTC" ); // Or "Asia/Kolkata" etc.
ZonedDateTime zdt = localDateTime.atZone( zoneId );