【问题标题】:Timestamp conversion to instant in Java adds unnecessary time offset在 Java 中将时间戳转换为即时会增加不必要的时间偏移
【发布时间】:2017-11-10 14:49:36
【问题描述】:

我需要能够将从存储在“日期时间”字段中的 MySQL 数据库获取的数据转换为 Java ZonedDateTime 对象。

ZonedDateTime dt = ZonedDateTime.ofInstant(rs.getTimestamp("Start").toInstant(), UTC_ZONE_ID)

我遇到的问题是toInstant() 将本地时间偏移添加到我不需要的Timestamp 对象,因为日期时间已经以UTC 格式存储在数据库中。 所以当我运行以下代码时:

ZonedDateTime startDT = 
        ZonedDateTime.ofInstant(rs.getTimestamp("Start").toInstant(),Globals.LOCALZONEID);
System.out.println(rs.getTimestamp("start"));
System.out.println(rs.getTimestamp("start").toInstant());

我明白了:

2017-06-08 13:15:00.0
2017-06-08T17:15:00Z

我需要时间组件保持不变。

我找不到任何明显的问题解决方案,所以我在这里遗漏了什么吗?

【问题讨论】:

    标签: java mysql datetime utc timezone-offset


    【解决方案1】:

    Timestamp & Instant 始终采用 UTC

    我遇到的问题是 .toInstant() 将本地时间偏移添加到 Timestamp 对象

    不,它没有。

    两者都不能分配任何其他区域。

    不要将您的代码打包成一行。将每个步骤分成单独的行,以便您可以调试它们的值。

    java.sql.Timestamp ts = rs.getTimestamp("Start") ;  // Actually in UTC, but it's `toString` method applies JVM’s current default time zone while generating string.
    Instant instant = ts.toInstant() ;                  // Same moment, also in UTC.
    ZoneId z = ZoneId.of( "America/Montreal" ) ;        // Or call your global var: `Globals.LOCALZONEID`.
    ZonedDateTime zdt = instant.atZone( z );            // Same moment, same point on timeline, but with wall-clock time seen in a particular zone.
    

    之后,您可能会看到问题(或非问题)。如果没有,请编辑您的问题以显示每个变量的调试值。

    不要相信Timestamp::toString

    重要提示:java.sql.Timestamp::toString 方法的谎言。该方法在生成字符串时应用 JVM 当前的默认时区。实际值始终采用 UTC。避免这些麻烦的遗留类的众多原因之一。在您自己的机器上运行以下代码示例,以查看您的默认时区对Timestamp 的文本表示的影响。

    让我们模拟一下code running live in IdeOne.com。 IdeOne.com 上的 JVM 默认为 UTC/GMT,因此我们通过将默认值指定为 Pacific/Auckland 来覆盖默认值。

    Instant now = Instant.now() ;                       // Simulating fetching a `Timestamp` from database by using current moment in UTC.
    
    TimeZone.setDefault( TimeZone.getTimeZone( "Pacific/Auckland" ) ) ;
    ZoneId zoneIdDefault = ZoneId.systemDefault() ;
    ZoneOffset zoneOffset = zoneIdDefault.getRules().getOffset( now ) ;
    
    java.sql.Timestamp ts = java.sql.Timestamp.from( now ) ;  // Actually in UTC, but it's `toString` method applies JVM’s current default time zone while generating string.
    Instant instant = ts.toInstant() ;                  // Same moment, also in UTC.
    ZoneId z = ZoneId.of( "America/Montreal" ) ;        // Or call your global var: `Globals.LOCALZONEID`.
    ZonedDateTime zdt = instant.atZone( z );            // Same moment, same point on timeline, but with wall-clock time seen in a particular zone.
    

    当前默认时区:太平洋/奥克兰

    当前默认的 UTC 偏移量:太平洋/奥克兰 |总秒数:43200

    now.toString(): 2017-06-09T04:41:10.750Z

    ts.toString(): 2017-06-09 16:41:10.75

    instant.toString(): 2017-06-09T04:41:10.750Z

    z.toString(): 美国/蒙特利尔

    zdt.toString(): 2017-06-09T00:41:10.750-04:00[美国/蒙特利尔]

    避免使用旧的日期时间类

    在 java.time 包之外发现的旧日期时间类很麻烦、令人困惑、设计糟糕且有缺陷。尽可能避免使用它们。这包括java.sql.Timestamp

    您的JDBC 4.2 兼容driver 可以通过调用PreparedStatement::setObjectResultSet::getObject 直接寻址java.time 类型。

    myPreparedStatement.setObject( … , instant ) ;
    

    ……和……

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

    如果使用尚未更新到 JDBC 4.2 和 java.time 的 JDBC 驱动程序,请使用添加到旧类的新方法短暂转换为 java.sql.Timestampfrom ( Instant )toInstant() 等。但除了与数据库交换数据之外,还可以在 java.time 对象中完成所有实际工作(业务逻辑)。

    myPreparedStatement.setTimestamp( … , java.sql.Timestamp.from( instant ) ) ;
    

    ……和……

    Instant instant = myResultSet.getTimestamp( … ).toInstant() ;
    

    关于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 类?

    • Java SE 8Java SE 9 及更高版本
      • 内置。
      • 标准 Java API 的一部分,带有捆绑实现。
      • Java 9 添加了一些小功能和修复。
    • Java SE 6Java SE 7
      • 大部分 java.time 功能都在ThreeTen-Backport 中向后移植到 Java 6 和 7。
    • Android
      • java.time 类的 Android 捆绑包实现的更高版本。
      • 对于早期的 Android (ThreeTenABP 项目采用 ThreeTen-Backport(如上所述)。见How to use ThreeTenABP…

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

    【讨论】:

    • ZonedDateTime startDT = rs.getObject("datetime", Instant.class); - 不会为我编译
    • @AnKing,您使用的是哪个 Java 版本? Java 7 中引入了重载的泛型 getObject(String, Class<T>)
    • 当我这样做时: Instant zdt = rs.getObject("start", Instant.class); JVM 提出:java.sql.SQLException: Conversion not supported for type java.time.Instant
    • SQLException 听起来好像你还没有 JDBC 4.2 驱动程序,因为这应该是 Instant 开始转换的版本。我可能会重复一些已经说过的话,一个好的解决方案是获得这样一个新的驱动程序。
    【解决方案2】:

    java.sql.Timestamp 这样的旧类的设计存在很多问题。经常引起混淆的一件事是Timestamp.toString() 打印 JVM 时区中的时间,即使其中的时间 Timestamp 只是包含一个没有时区的时间点。具体来说,当Timestamp 等于 UTC 中的 2017-06-08T17:15:00Z 并且您在运行匹兹堡时间的计算机上打印它时(每年的这个时候与 UTC 的偏移量为 -4:00 ),隐式调用Timestamp.toString(),它读取JVM的时区并将时间打印为13:15:00.0,因为宾夕法尼亚州的这个时间等于Timestamp中的UTC时间。

    长话短说,别担心,您的TimestampInstant 都是正确的。

    【讨论】:

    • 我想我现在的问题是:我如何从数据库中获取“日期时间”字段并将其转换为 ZonedDateTime 对象,假设 DB 中有一个 UTC 时间而不在​​这些操作期间添加任何偏移量?
    • 我猜答案是:你按照你在问题中所做的那样做。
    • 但是如果我按照我的时间进行操作: 2017-06-13 11:15:00 (UTC) 存储在数据库中最终是 2017-06-13T15:15Z[Etc/UTC] (添加了 4 小时偏移)。
    • 很容易弥补给你错误时间戳的错误,但在这样做之前,你真的想首先尝试了解它为什么会发生。例如,您确定 MySQL 数据库向您显示的值是 UTC 吗?
    • 我正在使用 MySQL 工作台查看数据库表。当我记录时间:06/13/2017 06:15 AM,我首先像这样解析它: ZonedDateTime.parse(jFormattedTextField1.getText(), Globals.dtFormat) (DateTimeFormatter dtFormat = DateTimeFormatter .ofPattern("MM/dd/ yyyy hh:mm a") .withZone(LOCALZONEID);) 然后我使用 UTCZONEID 的 DateTimeFormatter 将其格式化为 UTC 并将其记录到数据库中。它在数据库中显示 2017-06-13 10:15:00(如预期)
    【解决方案3】:

    我认为这段代码 sn-p 回答了你的问题。这将接收本地时区的字符串,将其转换为 UTC,并将其存储在数据库中。

        //Getting the LocalDateTime Objects from String values
        DateTimeFormatter df = DateTimeFormatter.ofPattern("yyyy-MM-dd kk:mm"); 
        String txtStartTime = "2017-03-29 12:00";
    
        LocalDateTime ldtStart = LocalDateTime.parse(txtStartTime, df);
    
    
        //Convert to a ZonedDate Time in UTC
        ZoneId zid = ZoneId.systemDefault();
    
        ZonedDateTime zdtStart = ldtStart.atZone(zid);
        System.out.println("Local Time: " + zdtStart);
        ZonedDateTime utcStart = zdtStart.withZoneSameInstant(ZoneId.of("UTC"));
        System.out.println("Zoned time: " + utcStart);
        ldtStart = utcStart.toLocalDateTime();
        System.out.println("Zoned time with zone stripped:" + ldtStart);
        //Create Timestamp values from Instants to update database
        Timestamp startsqlts = Timestamp.valueOf(ldtStart); //this value can be inserted into database
        System.out.println("Timestamp to be inserted: " +startsqlts);
    
        //insertDB(startsqlts);
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2016-04-29
      • 2019-11-22
      • 2012-07-15
      • 2016-10-23
      • 2011-02-06
      • 2021-02-16
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多