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



关于这个主题的部分已经说了很多(和写了很多),但不是以全面、完整的方式,所以我们可以有一个"终极、涵盖所有"的解决方案供每个人使用。

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

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

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

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

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

  • 从TSTZ到java日期、时间、时间戳的任何映射(例如通过JDBC getTimestamp()调用)都不会起作用,因为我丢失了TZ
  • OracleJDBC驱动程序不提供任何将TSTZ映射到javaCalendar对象的方法(这可能是一个解决方案,但并不存在)
  • JDBC getString()可以工作,但Oracle JDBC驱动程序返回的字符串格式为
    '2013-01-02 03:04:05.060708+900',这不符合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. 我希望有第四个,很好的选择,但从网上看,所以-我看不到任何。

想法?意见?

更新

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

这就是我最终决定使用的:

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

感谢大家的回答,
B.

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

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

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

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

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

日期是一对数字。它不是字符串。因此,机器接口不应该包含由iso字符串表示的日期,尽管这很容易调试。如果连java都不能解析iso日期,你认为你的客户端能做什么?

如果你为你的客户设计了一个接口,想想他们如何解析它。并提前编写一个代码来显示这一点。

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

我不确定longValue()是否会返回local或GMT/UCT中的值。如果不是格林尼治标准时间,您应该能够将日历加载为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时间进行一行转换:

    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特定的功能!:-)

因为看起来没有什么神奇的方法可以做到这一点,所以最简单、最短的方法是#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"就足够了,尽管完美主义者可能会放更多的化妆品(regex当然可以优化),例如:rs.getString(col).replaceFirst(","T").replaceAll("、").reaceFirst("\+([0-9])\:"、"+0$1:");

B。

oracle的解决方案是从双重中选择SYSTIMESTAMP

最新更新