关于这个主题的部分已经说了很多(和写了很多),但不是以全面、完整的方式,所以我们可以有一个"终极、涵盖所有"的解决方案供每个人使用。
我有一个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信息
所以,以下是我剩下的选项:
-
使用JDBC getString(),并对其进行字符串操作以修复并使其符合ISO 8601。这很容易做到,但如果Oracle更改内部硬编码的getString()格式,则会有死亡的危险。此外,通过查看getString()源代码,使用getString(()似乎也会导致一些性能损失。
-
使用Oracle DB"toString"转换:"SELECT TO_CHAR(tstz…)EVENT_TIME…"。这很好用,但有两个主要缺点:
- 现在,每个SELECT都必须包含to_CHAR调用,这是一个很难记住和编写的问题
- 现在,每个SELECT都必须添加EVENT_TIME列"别名"(例如,需要将结果自动序列化到Json)
-
使用Oracle的TIMESTAMPTZ java类,并从其内部(文档化的)字节数组结构中手动提取相关值(即实现我自己的toString()方法,Oracle忘记在那里实现该方法)。如果Oracle更改内部结构(不太可能)并且需要相对复杂的功能来实现和维护,那么这是有风险的。
-
我希望有第四个,很好的选择,但从网上看,所以-我看不到任何。
想法?意见?
更新
下面给出了很多想法,但似乎没有合适的方法。就我个人而言,我认为使用方法#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