Java和MySQL中的时间戳和时区转换

我正在与位于与我的不同时区的服务器上开发一个带有MySQL数据库的Java应用程序,我正在尝试在我的数据库中使用DATETIME或TIMESTAMP。

在阅读问题后, 我应该使用字段’datetime’还是’timestamp’? 和MySQL文档一样 ,我认为TIMESTAMP对我来说更好,因为它将值转换为UTC进行存储,然后返回当前时区进行检索。

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

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

我就是这样做的,但后来我从Prepared Statements的Java文档中找到了这个并且非常困惑:

void setTimestamp(int parameterIndex,Timestamp x,Calendar cal)抛出SQLException

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

在此之前,我认为时间戳总是按UTC惯例。 为什么人们想要一个本地化的时间戳而不是它的本地化表示 ? 这对每个人来说都不会让人感到困惑吗?

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

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

日期时间处理是一个混乱

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

不幸的是,计算机行业基本上选择忽略日期工作的这个问题。 正如鉴于明显的需要,Unicode花了太长时间才被发明,因此行业也在解决日期时间处理问题的过程中起了作用。

不依赖于自上帝以来的计数

但我必须不同意其结论。 使用count-since-epoch并不是最好的解决方案。 使用count-since-epoch本质上容易混淆,容易出错且不兼容。

  • 人类无法读取long数并将其解读为日期时间。 因此,至少可以说,validation数据和调试变得复杂。
  • 你会用什么“数”? java.util.Date和Joda-Time使用的毫秒数 ? Postgres ,MySQL和其他数据库使用的微秒 ? Java 8中新的java.time包使用的纳秒 ?
  • 你会用哪个时代 ? UTC中1970年代的Unix时代很常见,但远非单数。 各种计算机系统已经使用了近二十个时代 。

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

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

乔达时间

据我所知, Joda-Time项目是第一个以彻底,称职和成功的方式对待日期时间的项目。 即便如此,Joda-Time的创作者并不完全满意。 他们继续在Java 8中创建java.time包 ,并使用另外三个项目扩展该工作。 Joda-Time和java.time共享相似的概念但是不同,每个都有一些优点。

数据库问题

具体来说,java.util.Date和.Calendar类缺少仅限日期的值,没有时间和时区。 并且他们缺少没有日期和时区的仅限时间的值。 在Java 8之前,Java团队添加了称为java.sql.Datejava.sql.Time类的hacks,它们是伪装成仅日期的日期时间值。 Joda-Time和java.time都通过提供LocalDate和LocalTime类来纠正它。

另一个具体问题是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包具有一些特定function,可以更好地与数据库通信日期时间数据。

我可以编写更多内容,但是可以通过搜索StackOverflow来获取这些信息,例如joda,java.time,sql timestamp和JDBC。

使用带有Postgres的 JDBC的Joda-Time示例。 Joda-Time使用不可变对象来实现线程安全 。 因此,我们不是改变实例(“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

在此之前,我认为时间戳总是按UTC惯例。 为什么人们想要一个本地化的时间戳而不是它的本地化表示? 这对每个人来说都不会让人感到困惑吗?

确实。 SQL标准定义了TIMESTAMP WITHOUT TIME ZONE ,它忽略并删除任何包含的时区数据。 我无法想象它的用处。 这位Postgres专家David E. Wheeler 在建议总是使用TIMESTAMP WITH TIME ZONE 说道 。 Wheeler引用了一个狭隘的技术exception(分区),甚至在保存到数据库之前,还说自己将所有值转换为UTC。

最佳做法是以UTC格式工作和存储数据,同时调整为本地化时区以呈现给用户。 有时您可能想要记住其本地化时区中的原始日期时间数据; 如果是这样, 除了转换为UTC 之外,还要保存该值。

方针

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

MySQL的

警告:我不使用MySQL(我是Postgres的那种人)。

根据版本8文档 , DATETIMETIMESTAMP这两种类型的不同之处在于,第一种类型缺少任何时区概念或从UTC偏移。 第二个使用伴随输入的任何时区或UTC偏移的指示将该值调整为UTC,然后存储它,并丢弃区域/偏移信息。

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

  • DATETIME TIMESTAMP WITHOUT TIME ZONE SQL标准TIMESTAMP WITHOUT TIME ZONE
  • TIMESTAMP WITH TIME ZONE 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对象。 使用getObjectsetObject方法。

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

恢复。

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

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

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

关于java.time

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

现在处于维护模式的Joda-Time项目建议迁移到java.time类。

要了解更多信息,请参阅Oracle教程 。 并搜索Stack Overflow以获取许多示例和解释。 规范是JSR 310 。

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

从哪里获取java.time类?

  • Java SE 8Java SE 9Java SE 10及更高版本
    • 内置。
    • 带有捆绑实现的标准Java API的一部分。
    • Java 9增加了一些小function和修复。
  • Java SE 6Java SE 7
    • 许多java.timefunction都被反向移植到ThreeTen-Backport中的 Java 6和7。
  • Android的
    • 更高版本的Android捆绑java.time类的实现。
    • 对于早期的Android(<26), ThreeTenABP项目采用ThreeTen-Backport (如上所述)。 请参见如何使用ThreeTenABP ….

ThreeTen-Extra项目使用其他类扩展了java.time。 该项目是未来可能添加到java.time的试验场。 您可以在这里找到一些有用的课程,如IntervalYearWeekYearQuarter等。

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

我的回答很局部,但我会解释原因。

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

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

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