将 java.sql.Timestamp 保存到 mysql datetime 列会更改时间



我正在开发一个使用MySQL数据库的Spring Boot REST API。我正在尝试保存从客户端应用程序"按原样"收到的日期(带时间),该日期采用 UTC 时间。基本上,已经同意日期将以 UTC 来回发送,并且到适当时区的转换将在客户端上完成,所以我已经在 JSON 中拥有 UTC 时间,我正在尝试将其保存到 MySQL 中的 DATETIME 列。

我之所以选择DATETIME,是因为从MySQL参考中,它说TIMESTAMP转换值,而DATETIME没有,这正是我所需要的。

我已经创建了所有必需的实体来映射我的表,并且再次从参考中,我看到DATETIME映射到java.sql.Timestamp,这就是我用作类型的内容。

我正在使用 JpaRepository 并调用它的保存方法来保存我的实体。发生的情况是:当我将日期保存到MySQL时,它们会转换为UTC(我假设,因为我在UTC + 1中并且日期比我插入的日期早一小时)。

我已经阅读了有关SO的几个答案,并且尝试在我的jdbc连接字符串中使用以下属性(如此处建议):noDatetimeStringSync=trueuseLegacyDatetimeCode=falsesessionVariables=time_zone='-00:00',但它们都不起作用。

我不知道春天是不是做了什么弄乱了这件事?

这是我在请求中收到的 JSON 的一部分:

{
"dateFrom": "2019-12-01 16:00:00",
"dateTo": "2019-12-01 16:30:00"
}

我的实体具有以下属性:

import java.sql.Timestamp;
...
private Timestamp dateFrom;
private Timestamp dateTo;

数据库中的表列:

date_from   datetime
date_to     datetime

当我将字符串转换为时间戳并调用 save() 方法时,实体内部具有正确的值。所以JpaRepository和数据库本身之间的一些东西弄乱了日期。更新:时间戳实际上不是正确的值,它在从字符串转换时添加了ZoneInfo with ID="Europe/Prague"zoneOffset=3600000

我最终得到的是 db 中的2019-12-01 15:00:002019-12-01 15:30:00

我想要的是完全按照我收到它们的方式存储这些日期。我什至考虑过切换到 VARCHAR,因为我感到非常沮丧,但我不想这样做,因为我需要根据日期等执行查询。

有什么方法可以实现我想要的东西吗?起初看起来很简单,但现在它真的让我发疯了。

[如果需要,请提供其他信息] 我正在使用:

  • MySQL 5.7.27
  • 弹簧启动(启动器-父级)2.2.0.发布
  • MySQL-connector-java

编辑:

我可能以错误的方式做这件事,所以如果你能建议我如何以不同的方式做到这一点,那就太棒了。关键是:我的应用程序需要为不同时区的用户工作,他们需要通过日期相互交流。因此,如果时区欧洲/布拉格的一个用户对美国/芝加哥的另一个用户说"让我们明天下午5点谈谈",我需要以一种可以在当地时间为美国用户翻译的方式存储此信息(也可以为任何其他时区的任何其他用户)。这就是为什么我选择以 UTC 格式存储日期,然后在客户端将它们转换为用户的本地时间。但显然我对这一切是如何运作的误解了。

感谢 deHaar 和所有在评论中做出贡献的人,我终于让它按照我想要的方式工作,所以我将发布一个总结的答案,以防其他人可能需要它。

关键是在JSON请求/响应中使用标准化的日期字符串,以及正确的Java 8 DateTime对象。我最初发送的内容根本不够。

因此,根据ISO-8601日期和时间格式:

时间以 UTC(协调世界时)表示,并带有特殊的 UTC 指示符 ("Z")。

请注意,"T"字面意思出现在字符串中,表示 ISO 8601 中指定的时间元素的开头。

这意味着,我的 API 应该只在这些标准化的 UTC 日期字符串中进行通信,而不是自定义日期字符串的东西,例如:

{
"dateFrom": "2019-12-01T16:00:00Z",
"dateTo": "2019-12-01T16:30:00Z"
}

正如评论中提到的,我所做的另一件事是将数据库时区设置为 UTC,没有任何机会。由于我使用弹簧启动,因此有两种方法可以做到这一点:

  1. application.properties集合属性中:spring.jpa.properties.hibernate.jdbc.time_zone=UTC

  2. 或者在 jdbc 连接字符串中:serverTimezone=UTC

我还在上面的字符串中添加了useLegacyDatetimeCode=false参数,因为我读到没有它,serverTimezone参数不起作用(如果我错了,有人可以自由纠正我)。

现在,java.sql.Timestamp是否有时区信息存在一些争议。我浏览了SO以阅读更多有关它的信息,事实证明以前的版本(在Java 8之前)确实没有时区信息(例如这里)。但是,正如我在调试器中清楚地看到的那样,时间戳对象具有单独存储的区域信息,但它确实具有它。

根据评论中的马克·罗特维尔

a java.sql.timestamp 按规范表示默认 JVM 时区中的时间

这是有道理的,因为当我更改 JVM 时区时,时间戳值也发生了变化。

现在,为了解决这个问题,我看到了很多将默认时区设置为 UTC 的建议(因为如果没有设置,它自然会回退到 JVM)。我也使用以下代码尝试了这个

System.setProperty("user.timezone", "UTC");

它之所以有效,是因为它现在将数据库值转换为 UTC,但我不喜欢这种方法的是它将所有内容更改为 UTC 时间 (duh),包括我的日志。像这样"强行"地做这件事感觉不自然。

另一个对我来说非常有意义的发现是,每个人都一直在说要完全切换到java.time及其类,这使得SO上80%的答案非常不推荐使用且无用,因为他们建议使用旧类来做解析,转换等事情(仍然不确定我是否能够完全放弃java.sql.Timestamp因为现在文档说这是从 DB 中的datetime格式映射到的类型,但现在我将保留它)。

最后,我所做的是创建一个小的实用程序类来帮助我进行所需的转换。

import java.sql.Timestamp;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
public class DateTimeUtil {
public static Timestamp getTimestamp(String utcDateTime) {
LocalDateTime localDateTime = LocalDateTime.parse(utcDateTime, DateTimeFormatter.ISO_DATE_TIME);
return Timestamp.from(localDateTime.atZone(ZoneId.of("UTC")).toInstant());
}
public static String getUTCString(Timestamp dbTimestamp) {
return dbTimestamp.toInstant().atZone(ZoneId.of("UTC")).toString();
}
}

第一个函数用于解析来自 JSON 请求的字符串日期。

我选择了LocalDateTime而不是ZonedDateTime,因为java.time文档中的这些信息:

在可能的情况下,建议使用没有时区的更简单的类。时区的广泛使用往往会增加应用程序的复杂性。

(编辑:代码已更新,使用LocalDateTime是不正确的,因为它剥离了UTC的时区,然后表现得与JVM情况完全相同。抱歉,我测试错了)

第二个函数用于以 JSON 格式发送响应。当这些值以应有的方式存储在数据库中时,当我检索它们时,它们被转换为我的本地时区(因为 JVM 位于不同的时区)。这就是为什么需要说"嘿,这个值是UTC时区,这样显示,而不是在我的本地时区"。

所以现在,在将我的实体保存到数据库之前,我用getTimestamp()设置了它的时间戳,将其保存为 UTC,当我检索值并准备我的 DTO fro 响应时,我使用getUTCString()这也使其成为 ISO 格式的 UTC,而数据库的行为就像它位于 UTC 中一样,因此它无法将自己的内容添加到组合中,并且客户端和 API 之间的通信是标准化,每个人都确切地知道会发生什么。

如果你想确保你正在处理UTC时间,那么你应该将它们存储在数据库中。您可以在 Java 中指定区域或偏移量:

public ZonedDateTime utcDateTimeFrom(Timestamp timestamp) {
return timestamp.toLocalDateTime().atZone(ZoneId.of("UTC"));
}
public Timestamp toUtcTimestamp(LocalDateTime localDateTime) {
return Timestamp.from(localDateTime.atZone(ZoneId.of("UTC")).toInstant());
}

尝试这些方法或编写类似的方法(也有OffsetDateTime),但我认为您的 JSON 响应/请求中的日期时间是不够的,您还必须从客户端发送时区或偏移量,否则您可能会遇到时区问题。

将数据库的 DateTime 转换为 java.sql.Timestamp 时使用应用程序的时区。您需要传递环境变量或在应用程序中设置它。

-Duser.timezone=America/Chicago

TimeZone.setDefault(TimeZone.getTimeZone("America/Chicago"));

编辑:

我认为你错过了一个非常重要的点。

java.sql.Timestamp只不过是一个长值,即自 1971 年以来经过的毫秒数。这与时区无关,仅当您想要获取字符串表示形式时才需要它们。

当您从某个字符串表示形式创建java.sql.Timestamp对象2019-12-01 16:00:00时,您的值会通过使用服务器时区而移动。

来回答你的第二个问题。

例如 - 1573251160是我写这个答案的时候。此值表示所有时区中的即时时间相同。

现在,需要将其转换为日期时间列以存储在数据库中,该数据库是与时区相关的字段。如果不知道时区,您根本无法选择时间,因为不同时区的时间会有所不同。不知道时区就无法转换。 例如:相同的值将转换为:

  1. 周五, 08 十一月 2019 22:12:40 GMT
  2. 周五, 09 十一月 2019 3:42:40 AM GMT+05:30

转换将取决于您的服务器在哪个时区上运行。如果您总是期望您的值以 UTC 格式存储,那么在 UTC 上运行您的服务器似乎是合理的。

最新更新