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

时间:2013-07-09 22:55:00

标签: java database datetime jdbc timezone

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

我有一个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 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改变内部结构(不太可能)并且需要相对复杂的功能来实现和维护,这是有风险的。

  4. 我希望有第四个,很棒的选择,但是从浏览网页到现在 - 我看不到任何内容。

  5. 想法?意见?

    更新

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

    这是我最终决定使用的:

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

    感谢大家的好回答,
      乙

7 个答案:

答案 0 :(得分:3)

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

答案 1 :(得分:1)

  

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

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

如果您使用的是Java SE 8并且拥有ojdbc8,则可以使用getObject(int, ZonedDateTime.class)。请注意,当您使用@Override protected void onResume() { super.onResume(); browser.onResume(); } @Override protected void onPause() { super.onPause(); browser.onPause(); } 时,您可能会受到bug 25792016的影响。

  

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

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

答案 2 :(得分:0)

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

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

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

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

答案 3 :(得分:0)

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

我不确定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;
}

答案 4 :(得分:0)

你真的关心亚毫秒精度吗?如果没有从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特定功能呢! : - )

答案 5 :(得分:0)

因为看起来没有神奇的方法可以做到这一点,所以最简单和最短的方法是#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:”);

答案 6 :(得分:-4)

oracle的解决方案是SELECT SYSTIMESTAMP FROM DUAL