【问题标题】:Oracle / JDBC: retrieving TIMESTAMP WITH TIME ZONE value in ISO 8601 formatOracle / JDBC:以 ISO 8601 格式检索 TIMESTAMP WITH TIME ZONE 值
【发布时间】:2013-07-07 17:37:44
【问题描述】:

关于该主题的某些部分已经说了很多(并写在 SO 上),但不是以全面、完整的方式,因此我们可以有一个“终极的、包罗万象的”解决方案供所有人使用。

我有一个 Oracle DB,我在其中存储全局事件的日期+时间+时区,因此必须保留原始 TZ,并根据请求将其交付给客户端。理想情况下,它可以通过使用标准 ISO 8601“T”格式很好地工作,该格式可以使用“TIMESTAMP WITH TIME ZONE”列类型(“TSTZ”)很好地存储在 Oracle 中。

类似于 '2013-01-02T03:04:05.060708+09:00'

我需要做的就是从数据库中检索上述值并将其发送给客户端,无需任何操作。

问题是Java缺乏对ISO 8601(或任何其他日期+时间+nano+tz数据类型)的支持,情况更糟,因为Oracle JDBC驱动程序(ojdbc6.jar)对TSTZ的支持更少(与得到很好支持的 Oracle DB 本身相反)。

具体来说,这是我不应该或不能做的事情:

  • 任何从 TSTZ 到 java 日期、时间、时间戳的映射(例如,通过 JDBC getTimestamp() 调用)都不起作用,因为我丢失了 TZ。
  • Oracle JDBC 驱动程序不提供将 TSTZ 映射到 java Calendar 对象的任何方法(这可能是一个解决方案,但它不存在)
  • JDBC getString() 可以工作,但 Oracle JDBC 驱动程序返回格式为
    '2013-01-02 03:04:05.060708 +9:00' 的字符串,这不符合 ISO 8601(没有“T” , TZ 中没有尾随 0 等)。此外,这种格式在 Oracle JDBC 驱动程序实现中是硬编码的 (!),它也会忽略 JVM 区域设置和 Oracle 会话格式设置(即它忽略 NLS_TIMESTAMP_TZ_FORMAT 会话变量)。
  • JDBC getObject() 或 getTIMESTAMPTZ() 都返回 Oracle 的 TIMESTAMPTZ 对象,这实际上是没有用的,因为它没有任何转换为​​ Calendar(只有 Date、Time 和 Timestamp),所以我们再次丢失了 TZ 信息.

所以,这里是我剩下的选项:

  1. 使用 JDBC getString() 并对其进行字符串操作以修复并使其符合 ISO 8601。这很容易做到,但如果 Oracle 更改内部硬编码的 getString() 格式,就会有死亡的危险。此外,通过查看 getString() 源代码,似乎使用 getString() 也会导致一些性能损失。

  2. 使用 Oracle DB “toString” 转换:“SELECT TO_CHAR(tstz...) EVENT_TIME ...”。这很好用,但有两个主要缺点:

    • 现在每个 SELECT 都必须包含 TO_CHAR 调用,这让人头疼,难以记住和编写
    • 现在每个 SELECT 都必须添加 EVENT_TIME 列“别名”(例如需要自动将结果序列化为 Json)
      .
  3. 使用 Oracle 的 TIMESTAMPTZ java 类并从其内部(记录的)字节数组结构中手动提取相关值(即实现我自己的 toString() 方法,Oracle 忘记在那里实现)。如果 Oracle 改变内部结构(不太可能)并且需要相对复杂的功能来实现和维护,这是有风险的。

  4. 我希望有第 4 个不错的选择,但是从整个网络上看,我都看不到任何东西。

想法?意见?

更新

下面给出了很多想法,但似乎没有合适的方法去做。就我个人而言,我认为使用方法#1 是最短且最易读的方法(并且保持了不错的性能,不会丢失亚毫秒或基于 SQL 时间的查询能力)。

这是我最终决定使用的:

String iso = rs.getString(col).replaceFirst(" ", "T");

感谢大家的好回答,
B.

【问题讨论】:

  • “我只需要...”这里是龙。
  • 我看到的最大挑战是TSTZ types 可以是日期时间偏移量,例如您给出的2013-01-02T03:04:05.060708+09:00 示例,也可以是日期时间时区类型,例如2013-01-02T03:04:05.060708 Asia/Tokyo。即使使用Joda Time,这也是两种不同的数据类型。而且我不确定您是否可以直接将 Joda 与 Oracle 一起使用,而无需从 Java 原生类型开始。
  • @Matt Johnson,因为我控制插入和检索到/从 DB,我可以限制我的系统只支持 +HH:MM TZO 并禁止 TZ 名称。至于 Joda 等,你是对的 - 它会被 Oracle 驱动程序从 DB 搞砸,而 Joda 也无济于事。谢谢。
  • 我刚刚发现 this article 讨论了这个确切的问题并提供了一些解决方案。也许会有所帮助。
  • 谢谢马特,我前一阵子看到了,它确实干净易读。那里的困境与我所面临的相似。它要么使用极其复杂、繁重和错误的日历 API(24 行代码用于插入,类似的用于检索,外加一些变通方法),或者只是使用字符串,作为我的选项#1,具有所有优点和缺点。谢谢。

标签: java database datetime jdbc timezone


【解决方案1】:

JDBC getObject() 或 getTIMESTAMPTZ() 都返回 Oracle 的 TIMESTAMPTZ 对象,这实际上是没有用的,因为它没有任何转换为​​ Calendar(只有 Date、Time 和 Timestamp),所以我们再次丢失了 TZ 信息.

这是我的建议,作为获取您所寻求信息的唯一可靠方式。

如果您使用的是 Java SE 8 并且拥有 ojdbc8,那么您可以使用 getObject(int, OffsetDateTime.class)。请注意,当您使用getObject(int, ZonedDateTime.class) 时,您可能会受到bug 25792016 的影响。

使用 Oracle 的 TIMESTAMPTZ java 类并从其内部(记录的)字节数组结构中手动提取相关值(即实现我自己的 toString() 方法,Oracle 忘记在那里实现)。如果 Oracle 改变内部结构(不太可能)并且需要相对复杂的功能来实现和维护,这是有风险的。

这就是我们ultimately went with 在 Oracle JDBC 驱动程序中提供无错误 JSR-310 支持之前的内容。我们确定这是获取所需信息的唯一可靠方法。

【讨论】:

  • 我们也使用 ojdbc7 的自定义映射实现,并且在某些 TZ 星座中存在严重错误(映射到 UTC 时间未正确完成)。谢谢链接!
【解决方案2】:

oracle 的解决方案是 SELECT SYSTIMESTAMP FROM DUAL

【讨论】:

    【解决方案3】:

    由于看起来没有神奇的方法可以正确地做到这一点,因此最简单和最短的方法是#1。具体来说,这就是所有需要的代码:

    // convert Oracle's hard-coded: '2013-01-02 03:04:05.060708 +9:00'
    // to properly formatted ISO 8601: '2013-01-02T03:04:05.060708 +9:00'
    String iso = rs.getString(col).replaceFirst(" ", "T"); 
    

    似乎只添加'T'就足够了,尽管完美主义者可能会添加更多化妆品(当然,正则表达式可以优化),例如:rs.getString(col).replaceFirst(" ", "T")。 replaceAll(" ", "").replaceFirst("\+([0-9])\:", "+0$1:");

    B.

    【讨论】:

      【解决方案4】:

      对#2略有改进:

      CREATE OR REPLACE PACKAGE FORMAT AS
        FUNCTION TZ(T TIMESTAMP WITH TIME ZONE) RETURN VARCHAR2;
      END;
      /
      CREATE OR REPLACE PACKAGE BODY FORMAT AS
        FUNCTION TZ(T TIMESTAMP WITH TIME ZONE) RETURN VARCHAR2
        AS
        BEGIN
          RETURN TO_CHAR(T,'YYYYMMDD"T"HH24:MI:SS.FFTZHTZM');
        END;
      END;
      /
      

      在 SQL 中变成:

      SELECT FORMAT.TZ(tstz) EVENT_TIME ...
      

      它更具可读性。
      如果您需要更改它,它是 1 个地方。
      缺点是它是一个额外的函数调用。

      【讨论】:

      • 感谢 Devon,这是一个可读性很好的代码,但即使没有它,如果添加用于调整会话变量控制格式的 DB 登录触发器,TO_CHAR(TSTZ) 也可以正常工作。请参阅此处 (stackoverflow.com/a/447965/59770)。我使用这种方法,它就像一个魅力(虽然它不会解决主题中的 JDBC 问题)
      【解决方案5】:

      您真的关心亚毫秒级精度吗?如果不从 UTC 毫秒 + 时区偏移量转换为您所需的字符串,则使用 joda-time 进行单行:

          int offsetMillis = rs.getInt(1);
          Date date = rs.getTimestamp(2);
      
          String iso8601String =
                  ISODateTimeFormat
                          .dateTime()
                          .withZone(DateTimeZone.forOffsetMillis(offsetMillis))
                          .print(date.getTime());
      

      例如打印(当前时间+9:00):

      2013-07-18T13:05:36.551+09:00
      

      关于数据库:两列,一列用于偏移量,一列用于日期。日期列可以是实际的日期类型(因此可以使用许多独立于时区的数据库日期函数)。对于时区相关的查询(例如提到的全球每小时直方图),视图可能会公开列:local_hour_of_day、local_minute_of_hour 等。

      如果没有可用的 TSTZ 数据类型,这很可能是一个人必须这样做的方式——考虑到 Oralce 的不良支持,实际情况几乎就是这种情况。无论如何,谁想使用 Oracle 的特定功能! :-)

      【讨论】:

      • 基思,我确实关心亚毫米。此外,TSTZ 列允许在 SQL 查询中使用内置的 TSTZ 算术,这是一个很好的附加功能。具有讽刺意味的是,Oracle DB 本身确实对 TSTZ 有很好的支持(并且在任何现代 DB 中都对 TSTZ 提供了极好的支持)。问题在于写得非常糟糕的 Oracle 的 JDBC 驱动程序。谢谢。
      【解决方案6】:

      这是未经测试的,但似乎应该是一种可行的方法。我不确定是否将 TZ 名称解析出来,但只是将 TZTZ 对象的两个部分视为 Calendar 的单独输入似乎是可行的。

      我不确定 longValue() 是否会返回本地值或 GMT/UCT 值。如果不是 GMT,您应该能够将日历加载为 UTC,并要求它提供转换为本地 TZ 的日历。

      public Calendar toCalendar(oracle.sql.TIMESTAMPTZ myOracleTime) throws SQLException {
          byte[] bytes = myOracleTime.getBytes();
          String tzId = "GMT" + ArrayUtils.subarray(bytes, ArrayUtils.lastIndexOf(bytes, (byte) ' '), bytes.length);
          TimeZone tz = TimeZone.getTimeZone(tzId);
          Calendar cal = Calendar.getInstance(tz);
          cal.setTimeInMillis(myOracleTime.longValue());
          return cal;
      }
      

      【讨论】:

      • 这与我描述的选项 #3 的方向相同(获取 TSTZ 对象并使用自定义代码解析它)。这种方法有两个问题:(a)你损失了纳秒(和微秒); (b) 实际的 TSTZ 字节结构更复杂,您需要应用一些额外的操作才能使其工作。看看 Oracle 自己是如何做到这一点的:(look for getString)。我认为这是可能的,但对于这样一个简单的任务来说有点太重(而且有点冒险)。谢谢。
      【解决方案7】:

      您需要两个值:自 1970 年以来以毫秒为单位的 utc 时间和 utc 的时区偏移量。
      因此,将它们作为一对存储并作为一对转发。

      class DateWithTimeZone {
      long timestampUtcMillis;
      // offset in seconds
      int tzOffsetUtcSec;
      }
      

      日期是一对数字。它不是字符串。因此,机器接口不应包含由 iso 字符串表示的日期,尽管这便于调试。 如果连 java 都无法解析那个 iso 日期,你觉得你的客户怎么办?

      如果您为客户设计一个界面,请考虑他们如何解析它。并提前写一段代码来说明这一点。

      【讨论】:

      • 并将其作为什么数据类型存储在数据库中?我可以将它们作为结构类型、字节数组存储在数据库中,甚至可以将它们编码为大数字。但是所有这些想法都会阻止我在 SQL 查询中使用日期和时间(例如,我如何查询某个时间段的事件)?或者我如何按时间排序事件,TZ 感知?等等。这就像将原始 ISO 8601 字符串表示形式存储为数据库中的 VARCHAR2 - 不是很有用。谢谢。
      • 你知道按时间排序是什么意思?通过 UTC 订购!此外,在大多数情况下,您不应使用 tz 进行计算。全部存储在UTC,以UTC计算。仅在最后一步时,您必须在 ui 中显示本地时间的时间戳,然后使用 tz 转换为本地时间。重新思考你的设计。
      • 嗯,TZ 感知订单可能很简单,你是对的。但它可能会变得非常复杂。如何查询在下午 3 点到 5 点之间离开办公室(位于世界各地)的人数(或直方图)?使用内置 Oracle TSTZ 数据类型的最大好处是它为您提供了内置的整个日期/时间算术 API。另外,在 DB 中存储/检索内容时,我试图避免将序列化/反序列化代码写入 ISO 8601 格式/从 ISO 8601 格式写入。
      • 您可以使用 TSTZ 进行查询,但在 java 和所有其他系统中,他们不会喜欢这样。解析 iso8601 非常昂贵,而与 utcMillis 和 tzOffset 一起使用非常快。所以我不会将该 iso8601 转发到另一个系统。反序列化我在回答中给出的结构中的 TZTZ,可以让其他系统更快地处理数据。而且 iso8601 字符串需要更多的字节。其他一些数据库甚至在其 DATE 表示中没有 TZ 支持。
      • 在您的示例中,生成的直方图不应包含 x 轴上的 TZTZ,而应是自一天开始 (00:00) 以来的秒数。所以对于这样的特殊查询,如果你想转发那个结果,就必须创建特殊的接口。
      猜你喜欢
      • 2023-01-10
      • 2012-04-04
      • 1970-01-01
      • 2020-09-20
      • 2011-03-14
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-05-03
      相关资源
      最近更新 更多