简体   繁体   English

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

[英]Oracle / JDBC: retrieving TIMESTAMP WITH TIME ZONE value in ISO 8601 format

A lot have been said (and written on SO) on parts of the subject, but not in a comprehensive, complete way, so we can have one "ultimate, covering-it-all" solution for everyone to use. 关于部分内容已经说了很多(并且写在SO上),但不是全面,完整的方式,所以我们可以有一个“终极,覆盖所有”的解决方案供所有人使用。

I have an Oracle DB where I store date+time+timezone of global events, so original TZ must be preserved, and delivered to the client side upon request. 我有一个Oracle DB,我存储全局事件的日期+时间+时区,因此必须保留原始TZ,并根据请求传送到客户端。 Ideally, it could work nicely by using standard ISO 8601 "T" format which can be nicely stored in Oracle using "TIMESTAMP WITH TIME ZONE" column type ("TSTZ"). 理想情况下,它可以很好地使用标准的ISO 8601“T”格式,可以使用“TIMESTAMP WITH TIME ZONE”列类型(“TSTZ”)很好地存储在Oracle中。

Something like '2013-01-02T03:04:05.060708+09:00' “2013-01-02T03:04:05.060708 + 09:00”这样的东西

All I need to do is to retrieve the above value from DB and send it to client without any manipulations. 我需要做的就是从DB中检索上述值并将其发送到客户端而无需任何操作。

The problem is that Java lacks support of ISO 8601 (or any other date+time+nano+tz data type) and the situation is even worse, because Oracle JDBC driver (ojdbc6.jar) has even less support of TSTZ (as opposed to Oracle DB itself where it's well supported). 问题是Java缺乏对ISO 8601(或任何其他日期+时间+ nano + tz数据类型)的支持,情况更糟,因为Oracle JDBC驱动程序(ojdbc6.jar)对TSTZ的支持更少(相对于Oracle DB本身得到了很好的支持)。

Specifically, here's what I shouldn't or cannot do: 具体来说,这是我不应该或不能做的:

  • Any mapping from TSTZ to java Date, Time, Timestamp (eg via JDBC getTimestamp() calls) won't work because I lose TZ. 从TSTZ到Java Date,Time,Timestamp(例如通过JDBC getTimestamp()调用)的任何映射都不起作用,因为我丢失了TZ。
  • Oracle JDBC driver doesn't provide any method to map TSTZ to java Calendar object (this could be a solution, but it isn't there) Oracle JDBC驱动程序没有提供将TSTZ映射到Java Calendar对象的任何方法(这可能是一个解决方案,但它不存在)
  • JDBC getString() could work, but Oracle JDBC driver returns string in format JDBC getString()可以工作,但Oracle JDBC驱动程序以格式返回字符串
    '2013-01-02 03:04:05.060708 +9:00', which is not compliant with ISO 8601 (no "T", no trailing 0 in TZ, etc.). '2013-01-02 03:04:05.060708 +9:00',不符合ISO 8601(没有“T”,TZ没有尾随0等)。 Moreover, this format is hard-coded (!) inside Oracle JDBC driver implementation, which also ignores JVM locale settings and Oracle session formatting settings (ie it ignores NLS_TIMESTAMP_TZ_FORMAT session variable). 此外,此格式在Oracle JDBC驱动程序实现中是硬编码的(!),它还忽略了JVM区域设置和Oracle会话格式设置(即它忽略了NLS_TIMESTAMP_TZ_FORMAT会话变量)。
  • JDBC getObject(), or getTIMESTAMPTZ(), both return Oracle's TIMESTAMPTZ object, which is practically useless, because it doesn't have any conversion to Calendar (only Date, Time and Timestamp), so again, we lose TZ information. JDBC getObject()或getTIMESTAMPTZ()都返回Oracle的TIMESTAMPTZ对象,这实际上没用,因为它没有任何转换到Calendar(只有Date,Time和Timestamp),所以我们再次丢失TZ信息。

So, here are the options I'm left with: 那么,这是我留下的选项:

  1. Use JDBC getString(), and string-manipulate it to fix and make ISO 8601 compliant. 使用JDBC getString(),并对其进行字符串操作以修复并使ISO 8601兼容。 This is easy to do, but there's a danger to die if Oracle changes internal hard-coded getString() formatting. 这很容易做到,但如果Oracle更改内部硬编码的getString()格式,则存在死亡的危险。 Also, by looking at the getString() source code, seems like using getString() would also result in some performance penalty. 另外,通过查看getString()源代码,似乎使用getString()也会导致一些性能损失。

  2. Use Oracle DB "toString" conversion: "SELECT TO_CHAR(tstz...) EVENT_TIME ...". 使用Oracle DB“toString”转换:“SELECT TO_CHAR(tstz ...)EVENT_TIME ...”。 This works fine, but has 2 major disadvatages: 这很好,但有两个主要的不利因素:

    • Each SELECT now has to include TO_CHAR call which is a headache to remember and write 现在每个SELECT都必须包含TO_CHAR调用,这是一个令人头疼的记忆和写入
    • Each SELECT now has to add EVENT_TIME column "alias" (needed eg to serialize the result to Json automatically) 每个SELECT现在必须添加EVENT_TIME列“别名”(例如需要自动将结果序列化为Json)
      .
  3. Use Oracle's TIMESTAMPTZ java class and extract relevant value manually from its internal (documented) byte array structure (ie implement my own toString() method which Oracle forgot to implement there). 使用Oracle的TIMESTAMPTZ java类并从其内部(记录的)字节数组结构中手动提取相关值(即实现我自己的toString()方法,Oracle忘记在那里实现)。 This is risky if Oracle changes internal structure (unlikely) and demands relatively complicated function to implement and maintain. 如果Oracle改变内部结构(不太可能)并且需要相对复杂的功能来实现和维护,这是有风险的。

  4. I hope there's 4th, great option, but from looking all over the web and SO - I can't see any. 我希望有第四个,很棒的选择,但是从整个网络上看也是如此 - 我看不到任何东西。

Ideas? 想法? Opinions? 意见?

UPDATE UPDATE

A lot of ideas have been given below, but it looks like there is no proper way to do it. 下面给出了很多想法,但看起来没有正确的方法来做到这一点。 Personally, I think using method #1 is the shortest and the most readable way (and maintains decent performance, without losing sub-milliseconds or SQL time-based query capabilities ). 就个人而言,我认为使用方法#1是最短且最易读的方式(并保持良好的性能, 而不会丢失亚毫秒或基于SQL时间的查询功能 )。

This is what I eventually decided to use: 这是我最终决定使用的:

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

Thanks for good answers everyone, 感谢大家的好回答,
B. B.

A slight improvement to #2: 对#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;
/

The in SQL this becomes: 在SQL中这变为:

SELECT FORMAT.TZ(tstz) EVENT_TIME ...

It's more readable. 它更具可读性。
If you ever need to change it, it's 1 place. 如果您需要更改它,它是1个地方。
The downside is it is an extra function call. 缺点是它是一个额外的函数调用。

JDBC getObject(), or getTIMESTAMPTZ(), both return Oracle's TIMESTAMPTZ object, which is practically useless, because it doesn't have any conversion to Calendar (only Date, Time and Timestamp), so again, we lose TZ information. JDBC getObject()或getTIMESTAMPTZ()都返回Oracle的TIMESTAMPTZ对象,这实际上没用,因为它没有任何转换到Calendar(只有Date,Time和Timestamp),所以我们再次丢失TZ信息。

That would be my recommendation as the only reliable way to get the information you seek. 这将是我的建议,是获取所需信息的唯一可靠方式。

If you are on Java SE 8 and have ojdbc8 then you can use getObject(int, OffsetDateTime.class ). 如果您使用的是Java SE 8并且有ojdbc8,则可以使用getObject(int, OffsetDateTime.class )。 Be aware that when you use getObject(int, ZonedDateTime.class ) you may be affected by bug 25792016 . 请注意,当您使用getObject(int, ZonedDateTime.class )时,您可能会受到错误25792016的影响。

Use Oracle's TIMESTAMPTZ java class and extract relevant value manually from its internal (documented) byte array structure (ie implement my own toString() method which Oracle forgot to implement there). 使用Oracle的TIMESTAMPTZ java类并从其内部(记录的)字节数组结构中手动提取相关值(即实现我自己的toString()方法,Oracle忘记在那里实现)。 This is risky if Oracle changes internal structure (unlikely) and demands relatively complicated function to implement and maintain. 如果Oracle改变内部结构(不太可能)并且需要相对复杂的功能来实现和维护,这是有风险的。

This is what we ultimately went with until bug free JSR-310 support is available in the Oracle JDBC driver. 这是我们最终使用的,直到Oracle JDBC驱动程序中提供无错误的JSR-310支持。 We determined this was the only reliable way to get the information we want. 我们确定这是获取我们想要的信息的唯一可靠方式。

you need two values: time utc in millis since 1970 and timezone offset fom utc. 你需要两个值:自1970年以来以毫秒为单位的时间utc和时区偏移量fom utc。
So store them as a pair and forward them as a pair. 因此,将它们存储为一对并将它们作为一对转发。

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

A Date is a pair of numbers. 日期是一对数字。 It is not a String. 它不是一个字符串。 So a machine interface should not contain a date represented by a iso string, although that is handy to debug. 因此,机器接口不应包含由iso字符串表示的日期,尽管这对调试很方便。 If even java cannot parse that iso date, how do you think that your clients can do? 如果即使java无法解析iso日期,您认为您的客户怎么办?

If you design an interface to your clients, think how they can parse that. 如果您为客户设计了一个界面,请考虑如何解析它。 And in advance write a code that shows that. 并提前编写一个显示该代码的代码。

This is untested, but seems like it ought to be a workable approach. 这是未经测试的,但似乎它应该是一种可行的方法。 I'm not sure about parsing the TZ name out, but just treating the two parts of the TZTZ object as separate inputs to Calendar seems like the was to go. 我不确定解析TZ名称,但只是将TZTZ对象的两个部分视为Calendar的单独输入似乎就是这样。

I'm not sure whether longValue() will return the value in local or GMT/UCT. 我不确定longValue()是否会返回本地或GMT / UCT中的值。 If it's not GMT, you should be able to load a calendar as UTC and ask it for a Calendar converted to local TZ. 如果它不是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;
}

Do you really care about sub-millisecond precision? 你真的关心亚毫秒精度吗? If not converting from a UTC millisecond + timezone-offset to your required string is a one-liner using joda-time: 如果没有从UTC毫秒+时区偏移转换为您需要的字符串是使用joda-time的单线程:

    int offsetMillis = rs.getInt(1);
    Date date = rs.getTimestamp(2);

    String iso8601String =
            ISODateTimeFormat
                    .dateTime()
                    .withZone(DateTimeZone.forOffsetMillis(offsetMillis))
                    .print(date.getTime());

Prints, for example (current time in +9:00): 打印,例如(当前时间+9:00):

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

Regarding the database: Two columns, one for the offset, one for the date. 关于数据库:两列,一列用于偏移,一列用于日期。 The date column could be an actual date type (thus making many, timezone-independent anyway, db date functions available). 日期列可以是实际日期类型(因此,无论如何都可以创建许多,时区独立的数据库日期函数)。 For time-zone dependent queries (such as the mentioned global hourly histogram) perhaps a view could expose columns: local_hour_of_day, local_minute_of_hour, etc. 对于时区相关的查询(例如提到的全局每小时直方图),可能会有一个视图显示列:local_hour_of_day,local_minute_of_hour等。

This is likely how one would have to do it if no TSTZ datatype was available--which, considering Oralce's poor support, is the nearly the case for practical purposes. 如果没有可用的TSTZ数据类型,这可能是必须要做的 - 考虑到Oralce的不良支持,实际上几乎就是这种情况。 Who wants to use an Oracle specific features anyway! 谁想要使用Oracle特定功能呢! :-) :-)

Since it looks like there's no magical way of doing this right, the simplest and the shortest method would be #1. 由于看起来没有神奇的方法可以做到这一点,最简单和最短的方法是#1。 Specifically, this is all the code needed: 具体来说,这是所需的所有代码:

// 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"); 

it seems that just adding 'T' is enough, although a perfectionist would probably put more cosmetics (regex can optimized, of course), eg: rs.getString(col).replaceFirst(" ", "T").replaceAll(" ", "").replaceFirst("\\+([0-9])\\:", "+0$1:"); 似乎只添加'T'就足够了,虽然完美主义者可能会放更多的化妆品(正则表达式可以优化),例如:rs.getString(col).replaceFirst(“”,“T”)。replaceAll(“ “,”“).replaceFirst(”\\ +([0-9])\\:“,”+ 0 $ 1:“);

B. B.

oracle的解决方案是SELECT SYSTIMESTAMP FROM DUAL

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM