将 java.sql.Timestamp 保存到 mysql 日期时间列会更改时间
Posted
技术标签:
【中文标题】将 java.sql.Timestamp 保存到 mysql 日期时间列会更改时间【英文标题】:Saving java.sql.Timestamp to mysql datetime column changes the time 【发布时间】:2020-03-05 10:06:00 【问题描述】:我正在开发一个使用 mysql 数据库的 Spring Boot REST API。我正在尝试“按原样”保存从客户端应用程序接收到的日期(带时间),该日期是 UTC 时间。基本上,已经同意日期将在 UTC 中来回发送,并且将在客户端上完成到适当时区的转换,所以我已经有 JSON 中的 UTC 时间,我正在尝试将其保存到 DATETIME 列在 MySQL 中。
我选择 DATETIME 是因为 MySQL reference 中说 TIMESTAMP 转换值,而 DATETIME 没有,这正是我所需要的。
我已经创建了映射表所需的所有实体,from reference,我已经看到 DATETIME 映射到 java.sql.Timestamp,所以这就是我用作类型的内容。
我正在使用 JpaRepository 并调用它的 save 方法来保存我的实体。 发生的情况是:当我将日期保存到 MySQL 时,它们会转换为 UTC(我假设,因为我在 UTC+1 并且日期比我插入的时间早一小时)。
我已经阅读了几个关于 SO 的答案,并尝试在我的 jdbc 连接字符串中使用以下属性(如建议的here):noDatetimeStringSync=true
、useLegacyDatetimeCode=false
、sessionVariables=time_zone='-00:00'
但它们都不起作用。
我不知道是不是 spring 会搞砸这个?
这是我在请求中收到的 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;
db 中的表列:
date_from datetime
date_to datetime
当我将 String 转换为 Timestamp 并调用 save() 方法时,实体内部具有正确的值。所以 JpaRepository 和数据库本身之间的某些东西会与日期混淆。
更新: Timestamp 实际上不是正确的值,它在从 String 转换时添加了 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 Spring Boot (starter-parent) 2.2.0.RELEASE mysql-connector-java编辑:
我可能会以错误的方式处理这个问题,所以如果你能建议我如何以不同的方式做到这一点,那就太棒了。 重点是:我的应用需要为不同时区的用户工作,并且他们需要通过日期相互交流。因此,如果欧洲/布拉格时区的一个用户对美国/芝加哥的另一个用户说“让我们明天下午 5 点谈谈”,我需要以一种可以在当地时间为美国用户翻译的方式存储此信息(但也为任何其他时区的任何其他用户)。这就是为什么我选择在 UTC 中存储日期,然后在客户端将它们转换为用户的本地时间。但显然我误解了这一切是如何运作的。
【问题讨论】:
日期时间String
s 不包含任何有关时区或偏移的信息,将通过系统区域设置或数据库的默认区域设置进行解释。检查配置。
添加时区信息可以消除歧义。
Timestamp
变量中存储了什么?在哪个时区?
@deHaar 哦,好吧,这是有道理的,我认为它会“按原样”存储它而不包括任何时区。我可以以某种方式将其“强制”到 UTC 吗?我刚刚尝试将列类型更改为 TIMESTAMP,同样的事情发生了。
@OleV.V. JDBC 没有定义对Instant
的支持,这些是一些驱动程序的非标准扩展。
【参考方案1】:
在将数据库的 DateTime 转换为 java.sql.Timestamp 时使用应用程序的时区。您需要传递环境变量或在应用程序中设置它。
-Duser.timezone=America/Chicago
或
TimeZone.setDefault(TimeZone.getTimeZone("America/Chicago"));
编辑:
我认为你错过了一个非常重要的观点。
java.sql.Timestamp
只是一个 long 值,它是自 1971 年以来经过的毫秒数。这与时区无关,仅当您想要获取字符串表示时才需要它们。
当您从一些字符串表示创建java.sql.Timestamp
的对象时2019-12-01 16:00:00 使用您的服务器,您的价值本身就会发生变化> 时区。
来回答你的第二个问题。
例如 - 1573251160 是我写这个答案的时间。此值将意味着所有时区的相同即时时间。
现在需要将其转换为 DateTime 列以存储在数据库中,这是一个依赖于时区的字段。在不知道时区的情况下,您根本无法选择时间,因为不同时区的时间会有所不同。不知道时区就不可能转换。 例如:相同的值将被转换为:
-
格林威治标准时间 2019 年 11 月 8 日星期五 22:12:40
2019 年 11 月 9 日星期五凌晨 3:42:40 GMT+05:30
转换取决于您的服务器在哪个时区运行。如果您总是希望您的值以 UTC 格式存储,那么在 UTC 上运行您的服务器似乎是合理的。
【讨论】:
是否可以不使用任何转换但按原样阅读,因为我已经收到了 UTC 时间(这意味着我知道我的日期存储为 UTC,并将在客户端什么时候应该在他们的时区呈现给用户)?我觉得像这样设置时区会使转换为用户的本地时间变得困难。 "java.sql.Timestamp 只不过是一个 long 值,它是自 1971 年以来经过的毫秒数。" 虽然这是 实现 ,java.sql.Timestamp
按规范表示默认 JVM 时区中的时间。【参考方案2】:
如果您想确保处理的是 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 响应/请求中的日期时间不够,您必须从客户端发送时区或偏移量也一样,否则您可能会遇到时区问题。
【讨论】:
你说得对,我的 JSON 不好。经过更多研究,我决定使用 ISO-8601 标准并将“祖鲁时间”添加到我的字符串中。所以现在当我发送例如2019-12-01T12:00:00Z
它按应有的方式存储。我在检索该值时仍然遇到问题,因为 JDBC 或 Timestamp 不断将其转换回我的本地时间,但我综合了您的所有建议,最终让它以应有的方式工作。我将它作为答案发布只是因为它涵盖了我的问题中的所有内容,但是如果没有您的所有建议,我将无法做到。所以谢谢你!
@basarito 不客气...我期待您的解决方案 ;-)【参考方案3】:
感谢deHaar 和其他为 cmets 做出贡献的人,我终于让它按我的预期工作,所以我将发布一个总结答案,以防其他人可能需要它。
关键是在 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"
正如 cmets 中提到的,我做的另一件事是将 db 时区设置为 UTC,不留任何机会。由于我使用弹簧靴,有两种方法可以做到这一点:
在application.properties
设置属性:
spring.jpa.properties.hibernate.jdbc.time_zone=UTC
或者在jdbc连接字符串中:
serverTimezone=UTC
我还在上面的字符串中添加了useLegacyDatetimeCode=false
参数,因为我已经读过没有它,serverTimezone
参数无效(如果我错了,有人可以纠正我)。
现在,java.sql.Timestamp
是否具有时区信息存在一些争议。我通过 SO 阅读了更多关于它的信息,结果发现以前的版本(在 Java 8 之前)确实没有时区信息(例如 here)。但是,正如我在调试器中清楚地看到的那样,Timestamp 对象有单独存储的区域信息,但它确实有。
根据来自 cmets 的Mark Rotteveel
a java.sql.Timestamp 按规范表示默认 JVM 时区中的时间
这是有道理的,因为当我更改 JVM 时区时,时间戳值也发生了变化。
现在,为了解决这个问题,我看到了很多将默认时区设置为 UTC 的建议(因为如果没有设置,它自然会回退到 JVM 的)。我也尝试过使用以下代码
System.setProperty("user.timezone", "UTC");
它之所以有效,是因为它现在将 db 值转换为 UTC,但我不喜欢这种方法的是它把 everything 更改为 UTC 时间(duh),包括我的日志。像这样“强行”做这件事感觉不自然。
另一个对我来说非常有意义的发现是,每个人都一直说要完全切换到 java.time 及其类,这使得关于 SO 的 80% 的答案非常不推荐使用和无用,因为他们建议使用旧类来做事像解析,转换等(仍然不确定我是否也能够完全抛弃java.sql.Timestamp
,因为现在the docs说这是从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 格式发送响应。虽然这些值以它们应有的方式存储在 DB 中,但当我将它们取回时,它们被转换为我的本地时区(因为 JVM 位于不同的时区)。这就是为什么需要说“嘿,这个值是 UTC 时区,这样显示,而不是我的本地时区”。
所以现在,在将我的实体保存到 DB 之前,我使用 getTimestamp()
设置它的时间戳,将其保存为 UTC,当我检索值并准备我的 DTO 响应时,我使用 getUTCString()
这也使其成为 ISO格式化的 UTC,而 DB 的行为就像它位于 UTC 中一样,因此它不能在混合中添加自己的东西,并且客户端和 API 之间的通信是标准化的,每个人都知道会发生什么。
【讨论】:
以上是关于将 java.sql.Timestamp 保存到 mysql 日期时间列会更改时间的主要内容,如果未能解决你的问题,请参考以下文章
将 java.sql.Timestamp 转换为 Java 8 ZonedDateTime?
数据库中的java.sql.Timestamp转换成Date