Oracle / JDBC:以ISO 8601格式检索TIMESTAMP WITH TIME ZONE值

关于部分主题已经说了很多(并且写在SO上),但是没有全面,完整的方式,所以我们可以有一个“终极,覆盖所有”的解决方案供所有人使用。

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

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

我需要做的就是从DB中检索上述值并将其发送到客户端而无需任何操作。

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

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

  • 从TSTZ到Java Date,Time,Timestamp(例如通过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改变内部结构(不太可能)并且需要相对复杂的function来实现和维护,这是有风险的。

  4. 我希望有第四个,很棒的选择,但是从整个网络上看也是如此 – 我看不到任何东西。

想法? 意见?

UPDATE

下面给出了很多想法,但看起来没有正确的方法来做到这一点。 就个人而言,我认为使用方法#1是最短且最易读的方式(并保持良好的性能, 不会丢失亚毫秒或基于SQL时间的查询function )。

这是我最终决定使用的:

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

感谢大家的好评,
B.

对#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个地方。
缺点是它是一个额外的函数调用。

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

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

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

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

这是我们最终使用的,直到Oracle JDBC驱动程序中提供无错误的JSR-310支持。 我们确定这是获取我们想要的信息的唯一可靠方式。

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

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

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

如果您为客户设计了一个界面,请考虑如何解析它。 并提前编写一个显示该代码的代码。

这是未经测试的,但似乎它应该是一种可行的方法。 我不确定解析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; } 

你真的关心亚毫秒精度吗? 如果没有从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 

关于数据库:两列,一列用于偏移,一列用于日期。 日期列可以是实际日期类型(因此可以生成许多,时区无关,db日期函数可用)。 对于时区相关的查询(例如提到的全局每小时直方图),可能会有一个视图显示列:local_hour_of_day,local_minute_of_hour等。

如果没有可用的TSTZ数据类型,这可能是必须要做的 – 考虑到Oralce的不良支持,实际上几乎就是这种情况。 无论如何,谁想要使用Oracle特定function! 🙂

由于看起来没有神奇的方法来做到这一点,最简单和最短的方法将是#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.

oracle的解决方案是SELECT SYSTIMESTAMP FROM DUAL