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 ZONE
和TIMESTAMP 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
(一件坏事)。 这可能是您的主要困惑和麻烦。 你一直在考虑使用LocalDateTime
或ZonedDateTime
但你不应该使用它们; 相反,你应该使用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();
这很容易,从Instant
到java.sql.Timestamp
到TIMESTAMP WITH TIME ZONE
,全部是UTC。 没有涉及时区。 服务器操作系统,JVM和客户端的当前默认时区都无关紧要。
要向用户显示,请应用时区。 使用正确的时区名称 ,不要使用EST
或IST
等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
值,并且您完全确定了预期的偏移/区域上下文,那么:
- 将该字符串解析为
LocalDateTime
。 - 应用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.Date
, Calendar
和SimpleDateFormat
。
现在处于维护模式的Joda-Time项目建议迁移到java.time类。
要了解更多信息,请参阅Oracle教程 。 并搜索Stack Overflow以获取许多示例和解释。 规范是JSR 310 。
您可以直接与数据库交换java.time对象。 使用符合JDBC 4.2或更高版本的JDBC驱动程序 。 不需要字符串,不需要java.sql.*
类。
从哪里获取java.time类?
- Java SE 8 , Java SE 9及更高版本
- 内置。
- 带有捆绑实现的标准Java API的一部分。
- Java 9增加了一些小function和修复。
- Java SE 6和Java 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的试验场。 您可以在这里找到一些有用的课程,如Interval
, YearWeek
, YearQuarter
等。