【问题标题】:Timestamps and time zone conversions in Java and MySQLJava 和 MySQL 中的时间戳和时区转换
【发布时间】:2021-03-26 23:04:30
【问题描述】:

我正在与我的时区不同的服务器上开发一个带有 MySQL 数据库的 Java 应用程序,我正在尝试在我的数据库上使用 DATETIME 还是 TIMESTAMP 做出决定。

在阅读了Should I use field 'datetime' or 'timestamp'?MySQL documentation 之类的问题后,我认为 TIMESTAMP 更适合我,因为它将值转换为 UTC 进行存储,然后返回当前时区进行检索。

此外,正如用户 Jesper 在 this 线程中解释的那样,java.util.Date 对象在内部只是一个 UTC 时间戳(即自 Epoch 以来的毫秒数),并且当您执行 toString() 时,它会根据您当前的时区。

对我来说,这看起来是一个很好的做法:将日期时间存储为 UTC 时间戳,然后根据当前时区显示它们。

我正打算这样做,但后来我从Java documentation for Prepared Statements 中找到了这个并且非常困惑:

void setTimestamp(int parameterIndex, 时间戳 x, 日历校准) 抛出 SQLException

将指定参数设置为给定的 java.sql.Timestamp 值, 使用给定的日历对象。驱动程序使用日历对象 构造一个 SQL TIMESTAMP 值,然后驱动程序将其发送到 数据库。使用 Calendar 对象,驱动程序可以计算 考虑到自定义时区的时间戳。如果没有日历对象 指定时,驱动程序使用默认时区,即 运行应用程序的虚拟机。

在此之前,我认为时间戳始终采用 UTC 格式。为什么会有人想要一个本地化的时间戳而不是一个本地化的表示?这不会让每个人都感到很困惑吗?

这些转换是如何工作的?如果Java采用UTC时间戳并将其转换为任意时区,它如何告诉MySQL它在哪个时区?

MySQL 不会假设这个时间戳是 UTC,然后检索一个不正确的本地化值吗?

【问题讨论】:

    标签: java mysql timestamp


    【解决方案1】:

    日期时间处理一团糟

    answer by Teo 的第一段非常有见地且正确:Java 中的日期时间处理是一团糟。我知道的所有其他语言和开发环境也是如此。日期时间工作既困难又棘手,尤其容易出错和令人沮丧,因为我们直观地认为它是日期时间。但在数据类型、数据库、序列化、本地化、跨时区调整以及计算机编程附带的所有其他形式方面,“直观地”并没有削减它。

    不幸的是,计算机行业基本上选择忽略这个日期时间工作的问题。鉴于明显的需求,Unicode 花了很长时间才被发明出来,业界也在努力解决日期时间处理问题。

    不要依赖 Count-Since-Epoch

    但我必须不同意它的结论。使用 count-since-epoch 并不是最好的解决方案。使用 count-since-epoch 本质上是令人困惑、容易出错且不兼容的。

    我们创建数字数据类型是为了做数学而不是使用位。我们创建字符串类来处理处理文本而不是裸八位字节的细节。所以我们也应该创建数据类型和类来处理日期时间值。

    早期的 Java 团队(以及之前的 IBM 和 Taligent)尝试使用 java.util.Date 和 java.util.Calendar 以及相关类。不幸的是,这种尝试是不充分的。虽然日期时间本质上令人困惑,但这些类增加了更多的混乱。

    乔达时间

    据我所知,Joda-Time 项目是第一个以彻底、称职和成功的方式处理日期时间的项目。即便如此,Joda-Time 的创造者们并不完全满意。他们继续在 Java 8 中创建 java.time package,并使用 threeten-extra project 扩展该工作。 Joda-Time 和 java.time 具有相似的概念,但又各有优势。

    数据库问题

    具体来说,java.util.Date 和 .Calendar 类缺少没有时间和时区的仅日期值。而且它们缺少没有日期和时区的仅时间值。在 Java 8 之前,Java 团队添加了称为 java.sql.Datejava.sql.Time 类的黑客攻击,这是一个伪装成仅日期的日期时间值。 Joda-Time 和 java.time 都通过提供 LocalDateLocalTime 类来解决这个问题。

    另一个具体问题是 java.util.Date 的分辨率为毫秒,但数据库经常使用微秒或纳秒。为了弥合这种差异,早期的 Java 团队不明智地尝试创建另一个 hack,java.sql.Timestamp 类。虽然在技术上是 java.util.Date 子类,但它还跟踪小数秒到纳秒的分辨率。因此,在转换进出这种类型时,您可能会失去或获得更精细的小数秒粒度,而不会意识到这一事实。所以这可能意味着您期望相等的值不相等。

    另一个混淆来源是 SQL 数据类型TIMESTAMP WITH TIME ZONE。该名称用词不当,因为存储时区信息。将名称视为TIMESTAMP WITH RESPECT FOR TIME ZONE,因为任何传递的时区偏移信息都用于将日期时间值转换为UTC

    具有纳秒分辨率的 java.time 包具有一些特定功能,可以更好地与数据库通信日期时间数据。

    我可以写更多,但是可以通过在 StackOverflow 中搜索 joda、java.time、sql timestamp 和 JDBC 等词来收集这些信息。

    使用带有Postgres 的 JDBC 的 Joda-Time 示例。 Joda-Time 将immutable objects 用于thread-safety。因此,我们不是更改实例(“mutate”),而是根据原始值创建一个新实例。

    String sql = "SELECT now();";
    …
    java.sql.Timestamp now = myResultSet.getTimestamp( 1 );
    DateTime dateTimeUtc = new DateTime( now , DateTimeZone.UTC );
    DateTime dateTimeMontréal = dateTimeUtc.withZone( DateTimeZone.forID( "America/Montreal" ) );
    

    关注世界标准时间

    在此之前,我认为时间戳始终采用 UTC 格式。为什么会有人想要一个本地化的时间戳而不是它的本地化表示?这不会让每个人都感到很困惑吗?

    确实如此。 SQL 标准定义了一个TIMESTAMP WITHOUT TIME ZONE,它忽略并删除任何包含的时区数据。我无法想象它的用处。这位 Postgres 专家 David E. Wheeler,says as much in recommending 始终使用 TIMESTAMP WITH TIME ZONE。 Wheeler 引用了一个狭隘的技术例外(分区),即便如此,他还是说在保存到数据库之前自己将所有值转换为 UTC。

    最佳做法是使用 UTC 格式处理和存储数据,同时调整到本地时区以便向用户展示。有时您可能想记住本地时区中的原始日期时间数据;如果是这样,保存该值除了转换为UTC。

    指南

    更好地处理日期时间的第一步是避免使用 java.util.Date 和 .Calendar,使用 Joda-Time 和/或 java.time,专注于 UTC,并了解特定 JDBC 驱动程序的行为和特定的数据库(尽管有 SQL 标准,但数据库在日期时间处理方面差异很大)。

    MySQL

    警告:我不使用 MySQL(我是 Postgres 类型的人)。

    根据version 8 documentationDATETIMETIMESTAMP 这两种类型的不同之处在于第一种缺乏任何时区或与UTC 偏移的概念。第二个使用任何时区指示或从 UTC 偏移的输入,以将该值调整为 UTC,然后将其存储,并丢弃区域/偏移信息。

    所以这两种类型似乎类似于标准 SQL 类型:

    • MySQL DATETIME ≈ SQL 标准 TIMESTAMP WITHOUT TIME ZONE
    • MySQL TIMESTAMP ≈ SQL 标准 TIMESTAMP WITH TIME ZONE

    对于 MySQL DATETIME,使用 Java 类 LocalDateTime。该类与该数据类型一样,故意缺少任何时区或与 UTC 偏移的概念。使用此类型和类:

    • 当您指的是 任何 区域或 所有 区域时,例如“圣诞节从 2018 年 12 月 25 日的第一刻开始”。这意味着不同地方的不同时刻,因为东方比西方更早出现新的一天。
    • 当将约会或活动安排在足够远的未来时,政客们可能会更改时区的偏移量,世界各地的政客们对此都表现出一种倾向。在这种用法中,您必须在运行时应用时区来动态计算但不存储用于在日历上显示的时刻。这样,即使政客将时钟重新定义为提前或落后分钟/小时,8 个月内 15:00 的牙科预约仍然保持在 15:00。

    对于 MySQL TIMESTAMP,使用 Java 类 Instant,如上所示。将此类型和类用于时刻、时间轴上的特定点。

    JDBC 4.2

    从 JDBC 4.2 起,我们可以直接与数据库交换 java.time 对象。使用getObject & setObject 方法。

    myPreparedStatement.setObject( … , Instant.now() ) ;
    

    检索。

    Instant instant = myResultSet.getObject( … , Instant.class ) ;
    

    JDBC 4.2 规范要求驱动程序支持OffsetDateTime,但奇怪的是不需要支持更常见的类型InstantZonedDateTime。但是类型之间的转换非常容易。

    OffsetDateTime odt = myResultSet.getObject( … , OffsetDateTime.class ) ;
    Instant instant = odt.toInstant() ;
    

    然后,您可以将 Instant 中的 UTC 值调整为特定时区,以便呈现给用户。

    ZoneId z = ZoneId.of( "Pacific/Auckland" ) ;
    ZonedDateTime zdt = instant.atZone( z ) ;
    

    关于java.time

    java.time 框架内置于 Java 8 及更高版本中。这些类取代了麻烦的旧 legacy 日期时间类,例如 java.util.DateCalendarSimpleDateFormat

    Joda-Time 项目现在位于maintenance mode,建议迁移到java.time 类。

    要了解更多信息,请参阅Oracle Tutorial。并在 Stack Overflow 上搜索许多示例和解释。规格为JSR 310

    您可以直接与您的数据库交换 java.time 对象。使用符合JDBC 4.2 或更高版本的JDBC driver。不需要字符串,不需要java.sql.* 类。

    从哪里获取 java.time 类?

    ThreeTen-Extra 项目通过附加类扩展了 java.time。该项目是未来可能添加到 java.time 的试验场。您可以在这里找到一些有用的类,例如IntervalYearWeekYearQuartermore

    【讨论】:

    • 对于 2021 年阅读的人,我想指出带有 mysql 连接器 8 的 setObject(x, Instant.now()) 会引发 MysqlDataTruncation 异常,并带有消息 Data truncation: Incorrect datetime value: ...。但是使用 OffsetDateTime 工作正常
    • @Dario JDBC 4.2 规范要求驱动程序支持OffsetDateTime,但不支持InstantZonedDateTime。这是一个奇怪的选择,因为后两种类型更常用。不需要支持Instant 是莫名其妙的,因为在InstantOffsetDateTime 之间转换非常简单。一些 JDBC 驱动程序可能支持其他类型,但不是必需的。
    • 我明白了。我想知道您是否能够对我在 OffsetDateTime 中遇到的另一个问题进行排序。我使用 ZoneOffset.UTC 从瞬间创建一个新实例,但是当我在准备好的语句中设置对象时,数据库中的时间戳值保存在我的本地时间 (GMT+1) 中。当我通过查询将同一列提取到 OffsetDateTime 对象中时,我得到了带有本地时间偏移的值。我可以在我的客户端上将其转换为 UTC,但我想知道这实际上是正常的还是错误的?
    • @Dario (a) 您是否使用类似于 SQL 标准类型 TIMESTAMP WITH TIME ZONE 而不是 WITHOUT 的类型的列? (b) 我建议在提交给 JDBC 之前始终将您的 OffsetDateTime 调整为 UTC(零时分秒的偏移量),以避免特定 JDBC 驱动程序的任何错误行为。 (c) 您应该发布一个包含您的具体情况的问题。但您的问题可能已经在 Stack Overflow 上讨论过了。
    【解决方案2】:

    你的问题是我认为这些天来的一个巨大的问题。 DB(通过 SQL)和服务器端本身(通过 Java 等编程语言)都提供了处理日期和时间的方法概要。我会将现状定性为高度非标准化且有点混乱(个人意见:)

    我的回答是片面的,但我会解释原因。

    您是对的,Java 的日期(和日历)将时间存储为自 Unix 纪元以来的毫秒数(这很棒)。它不仅发生在 Java 中,还发生在其他编程语言中。在我看来,完美的计时架构由此自然而然地出现:Unix 纪元是 1970 年 1 月 1 日午夜,UTC。因此,如果您选择将时间存储为自 Unix 纪元以来的毫秒数,您将获得很多好处:

    • 架构清晰:服务器端使用 UTC,客户端通过其本地时区显示时间
    • 数据库简单性:您存储一个数字(毫秒),而不是像 DateTimes 这样的复杂数据结构
    • 编程效率:在大多数编程语言中,日期/时间对象在构建时能够从 Epoch 开始花费毫秒(如您所说,允许自动转换为客户端时区)

    我发现使用这种方法时代码和架构更简单、更灵活。我不再试图理解 DateTime(或 Timestamp)之类的东西,只在我必须修复遗留代码时才处理它们。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2011-06-02
      • 2015-04-24
      • 2013-04-15
      • 2011-07-07
      • 2017-11-05
      • 1970-01-01
      • 1970-01-01
      • 2011-03-29
      相关资源
      最近更新 更多