18

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

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

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

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

问题是 Java 缺乏对 ISO 8601(或任何其他 date+time+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 , ETC。)。此外,这种格式在 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。

4

7 回答 7

5

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

在 Oracle JDBC 驱动程序中提供无错误的 JSR-310 支持之前,我们最终采用了这种方式。我们确定这是获取所需信息的唯一可靠方法。

于 2017-04-24T05:57:33.627 回答
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 个地方。
缺点是它是一个额外的函数调用。

于 2013-07-18T18:48:43.047 回答
0

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

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

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

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

于 2013-07-12T00:46:25.370 回答
0

这是未经测试的,但似乎它应该是一种可行的方法。我不确定是否将 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;
}
于 2013-07-12T21:35:24.783 回答
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 的特定功能!:-)

于 2013-07-18T04:02:43.557 回答
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:");

B.

于 2013-07-18T23:15:12.327 回答
-5

oracle 的解决方案是 SELECT SYSTIMESTAMP FROM DUAL

于 2014-03-06T14:39:33.563 回答