我正在开发一个使用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=true
,useLegacyDatetimeCode=false
,sessionVariables=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:00
和2019-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,没有任何机会。由于我使用弹簧启动,因此有两种方法可以做到这一点:
在
application.properties
集合属性中:spring.jpa.properties.hibernate.jdbc.time_zone=UTC
或者在 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是我写这个答案的时候。此值表示所有时区中的即时时间相同。
现在,需要将其转换为日期时间列以存储在数据库中,该数据库是与时区相关的字段。如果不知道时区,您根本无法选择时间,因为不同时区的时间会有所不同。不知道时区就无法转换。 例如:相同的值将转换为:
- 周五, 08 十一月 2019 22:12:40 GMT
- 周五, 09 十一月 2019 3:42:40 AM GMT+05:30
转换将取决于您的服务器在哪个时区上运行。如果您总是期望您的值以 UTC 格式存储,那么在 UTC 上运行您的服务器似乎是合理的。