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

How to convert java.sql.Timestamp to java.time.OffsetDateTime?

我正在处理一个 Scala 项目,我需要将 OffsetDateTime 类型映射到 SQL Timestamp 类型。在数据库中,我想要 UTC 时间。

OffsetDateTimeTimestamp 的转换很简单(来自 的提示)并且按预期工作:

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,需要适配实际时区)

为什么?我做错了什么?

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

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

虽然java.sql.Timestamp存储了纪元毫秒,.toString方法使用默认时区来呈现字符串。此外,.valueOf 使用您的默认时区解释 LocalDateTime

两者结合,导致第一个转换为 "look" 正确,但实际上是错误的。值“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

引用的 Whosebug 问题的公认答案是错误的。

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