如何将 java.sql.Timestamp 转换为 java.time.OffsetDateTime?

11

我正在开发一个Scala项目,需要将OffsetDateTime类型映射到SQL的Timestamp类型。在数据库中,我希望使用UTC时间。

OffsetDateTime转换为Timestamp很简单(来自这个问题的提示),并且可以按预期工作:

import java.time._
import java.sql.Timestamp
val ofsdatetime = OffsetDateTime.now()
// ofsdatetime: java.time.OffsetDateTime = 2017-04-04T21:46:33.567+02:00

val tstamp = Timestamp.valueOf(ofsdatetime.atZoneSameInstant(ZoneOffset.UTC).toLocalDateTime())
// tstamp: java.sql.Timestamp = 2017-04-04 19:46:33.567

正如你所看到的,时区被移除并且时间戳是在时间上向前推了两个小时(UTC),非常好!

Timestamp 转换回 OffsetDateTime 意料之外地无法正常工作

OffsetDateTime.ofInstant(Instant.ofEpochMilli(tstamp.getTime), ZoneId.systemDefault())

// java.time.OffsetDateTime = 2017-04-04T19:46:33.567+02:00

新创建的OffsetDateTime已添加时区信息,但时间不正确(仍然是UTC),我需要它适应实际时区。

为什么?我做错了什么?

2个回答

13

java.sql.Timestamp是对表示自1970-01-01T00:00:00.000 UTC时刻以来的毫秒数的long值进行了薄包装,因此UTC时区在java.sql.Timestamp中是隐含的。它无法存储任何时区信息,但隐含地处于UTC时区,只要每个人都知道这一点,它就能正常工作。无法在java.sql.Timestamp中存储时区信息。如果您需要记住从输入数据中接收到的时区,请将其保存为数据库中的单独列。您可以在java.sql.Timestamp中保存正确的时间点,但不能保存输入数据中接收到的时区。为此,您需要额外的字段。

由于您希望DB中的日期处于UTC时区,因此您可以像这样从DB检索数据:OffsetDateTime.ofInstant(Instant.ofEpochMilli(tstamp.getTime), ZoneId.of("UTC"))。这将是正确的时间点,但在UTC时区中。在保存到DB之前,OffsetDateTime+0200时区中的事实无法从DB中检索出来,因为java.sql.Timestamp不存储时区组件。如果您需要该信息,您需要将其存储在DB的单独列中。


这是一个好答案,但你觉得我们应该直接调用Timestamp.toInstant()吗?你上面的代码调用了:Instant.ofEpochMilli(tstamp.getTime)。据我所知,我们会失去毫秒以下的精度。 - kevinarpe
1
这个答案并不完全正确。我要警告的是,不要用这种方式来解释它。java.sql.Timestamp / java.util.Date 没有任何与时区相关的东西,即使是隐含的。UTC 是一个时区,而纪元不关心时区,就是这样。请不要认为纪元是“隐含的 UTC”。 您可以使用 OffsetDateTime.ofInstant 与任何时区相结合,对一个瞬间进行操作,这将是“正确的时间点”,即底层纪元不会改变。 - taylorcressy
@taylorcressy "指定的毫秒数是自1970年1月1日00:00:00 GMT以来的标准基准时间,也称为“纪元”。" 来自 https://docs.oracle.com/javase/8/docs/api/java/util/Date.html - radumanolescu
1
@radumanolescu 是的,这一点是可以理解的。但是你在混淆事情,并且误解了文档。那是一个基于时区的参考,指的是“时钟”开始的时间。它也是自1969年12月31日16:00:00 GMT-08:00以来指定的毫秒数。纪元是一个时间点。它并不暗示它是UTC。你同样可以说“它隐含地处于PST时区”。你会得到相同的效果。 - taylorcressy
1
OffsetDateTime.ofInstant(Instant.ofEpochMilli(tstamp.getTime), ZoneId.of("UTC"))OffsetDateTime.ofInstant(Instant.ofEpochMilli(tstamp.getTime), ZoneId.of("America/Los_Angeles"))都是正确的。它们都是从数据库中检索日期的有效方法。这取决于应用程序所需的基于时区的表示方式。您的解释对于java.util.Date(epoch)的真实含义存在误解。 - taylorcressy

13
尽管java.sql.Timestamp存储的是epoch毫秒数,但.toString方法使用默认时区来呈现字符串。 此外,.valueOf使用您的默认时区解释LocalDateTime
这两种情况的组合会导致第一个转换“看起来”是正确的,但实际上是错误的。 值“2017-04-04 19:46:33.567”显示的是您的默认TZ而不是UTC。
因为您向valueOf方法传递了一个LocalDateTime(UTC),但它将其解释为LocalDateTime(您的默认TZ)。
这是第一个转换错误的证明:
scala> val now = OffsetDateTime.now
now: java.time.OffsetDateTime = 2017-04-04T14:50:12.534-06:00

scala> Timestamp.valueOf(now.atZoneSameInstant(ZoneId.of("UTC")).toLocalDateTime).getTime == now.toInstant.toEpochMilli
res54: Boolean = false

现在移除了 .atZoneSameInstant

scala> Timestamp.valueOf(now.toLocalDateTime).getTime == now.toInstant.toEpochMilli
res53: Boolean = true

所引用的stackoverflow问题的被接受答案是错误的。

一旦您修复第一个转换(删除.atZoneSameInstant),那么您的第二个转换应该可以正常工作。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接