1

我在我认为纯粹是 UTC 的代码中遇到了一个意外的夏令时问题。我正在使用 Java 1.6、iBatis SQL 映射器 (2.3.3) 和 Oracle XE(Oracle 10.2 的评估版)和 Oracle 瘦驱动程序。

该数据库包含一个表示电视广播时间表的表。每个“资产”(程序)都有一个开始时间和结束时间。这是相关的切片:

create table Asset
(
 asset_id      integer not null, -- The unique id of the Asset.
 [...] 
 start_time    timestamp,        -- The start time.
 end_time      timestamp,        -- The end time.
 [...] 

 constraint asset_primary_key    primary key (asset_id),
 constraint asset_time           check (end_time >= start_time)
);

asset_time在即将到来的 2009 年 11 月 1 日星期日上午,跨越美国中央夏令时调整的程序正在触发预言机约束。

我有这个数据传输对象(日期是 java.util.Dates):

public class Asset 
{
 protected Long    asset_id;
 [...]
 protected Date    start_time;
 protected Date    end_time; 

 public Date       getStart_time()     { return start_time; }
 public Date       getEnd_time()       { return end_time; }

 public void setStart_time(Date start_time) { this.start_time = start_time; }
 public void setEnd_time(Date end_time)     { this.end_time = end_time; }
 [...]
}

在 iBatis SQL 映射中,我有将 Asset DTO 插入 Oracle Asset 表的语句:

<insert id="Asset.insert" parameterClass="com.acme.Asset">
    insert into Asset 
        ( asset_id, [...] start_time, end_time )
    values
        ( #asset_id#, [...] #start_time#, #end_time# )
</insert>

在 Java 方面,我已经验证我通过这个预插入断言为 iBatis 提供了正确的 UTC 日期输入,它没有被抛出:

System.err.println("Inserting asset " + program_id);
System.err.println("  "+asset.getStart_time_str()+"--"+asset.getEnd_time_str());
if ( !asset.getEnd_time().after(asset.getStart_time())) {
 System.err.println("Invalid datetime range in asset.");
 throw new AssertionError("Invalid datetime range in asset.");
}

就在 Oracle 约束失败之前,上面的代码打印:

Inserting asset EP011453960004
  2009-11-01T06:30:00Z--2009-11-01T07:00:00Z

我在美国中部时区,格林威治标准时间 -5:00,所以这个程序从凌晨 1:30 开始,到凌晨 2:00 结束。夏令时更改在凌晨 2:00 发生,并将时钟调回凌晨 1:00。

iBatis 报告 Oracle 约束失败(已编辑):

2009-10-30 22:58:42,238  [...] Executing Statement:
    insert into Asset ( asset_id, [...] start_time, end_time )
         values       ( ?, [...] ?, ? )  
2009-10-30 22:58:42,238  [...] Parameters: 
    [EP011453960004, [...] 2009-11-01 01:30:00.0, 2009-11-01 01:00:00.0]
2009-10-30 22:58:42,238  [..] Types: 
    [java.lang.Long, [...] java.sql.Timestamp, java.sql.Timestamp]
2009-10-30 22:58:42,285  [...] - Failed with a SQLException:   
--- The error occurred in com/acme/data/dao/Asset-Write.xml.  
--- The error occurred while applying a parameter map.  
--- Check the Asset.insert-InlineParameterMap.  
--- Check the statement (update failed).  
--- Cause: java.sql.SQLException: ORA-02290: check constraint (ACME.ASSET_TIME)
                                             violated

您会注意到,在 Oracle 方面,它看到了带有夏令时调整的 start_time/end_time,因此 iBatis 映射逻辑或 Oracle 驱动程序中的某些内容没有达到我的预期。驱动是ojdbc14.jar,瘦驱动:

JDBCReadWrite.Driver        = oracle.jdbc.OracleDriver
JDBCReadWrite.ConnectionURL = jdbc:oracle:thin:@localhost:1521:XE

确保此代码纯粹是 UTC 的正确方法是什么?

提前致谢!

4

2 回答 2

6

我有一个似乎可以解决问题的解决方案。尽管应用程序和数据库使用的类型存储从 GMT 时间 1970 年 1 月 1 日午夜开始的时间偏移量,但 JDBC 规范要求对 JVM 的默认时区进/出进行调整。iBatis 使用 JDBC 默认映射日期。只要数据没有跨越夏令时边界,或者机器或 JVM 默认设置为 GMT,这些调整总是对称的,因此是无害的。

作为一个实验,我将 JVM 默认时区切换为 GMT:

TimeZone.setDefault(TimeZone.getTimeZone("UTC"));

这解决了这个问题,尽管是以一种非常严厉的方式(JVM中的其他代码可能不会期望这个)。

但是 iBatis 允许您在任何粒度级别覆盖默认类型处理。我编写了一个保留 GMT 的类型处理程序,并为我所有的 java.util.Dates 注册了它:

<typeHandler callback="com.acme.GMTDateTypeHandler" javaType="java.util.Date"/>

我的类型处理程序如下所示:

public class GMTDateTypeHandler implements TypeHandlerCallback
{     
    @Override
    public void setParameter(ParameterSetter setter, Object parameter) 
        throws SQLException
    {
        java.util.Date date = (java.util.Date) parameter;
        if ( date == null )
            setter.setNull(Types.TIMESTAMP);
        else
        {
            Timestamp timestamp = new Timestamp(date.getTime());
            Calendar calendar = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
            setter.setTimestamp(timestamp, calendar);
        }
    }

    @Override
    public Object getResult(ResultGetter getter) throws SQLException
    {
        Calendar calendar = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
        return getter.getTimestamp(calendar);
    }

    @Override
    public Object valueOf(String s)
    {
        throw new UnsupportedOperationException(
            "GMTDateTypeHandler.valueOf() is not supported.");
    }
}
于 2009-11-02T23:40:25.273 回答
0

通常,Oracle 在存储数据时会将日期/时间值从客户端时区转换为服务器时区。并且向后,当再次阅读它时。

如果您希望将日期/时间值原封不动地存储,您可能需要使用时区数据类型的变体,即“TIMESTAMP WITH TIME ZONE 数据类型”,它允许您使用值存储时区。您可以在Oracle SQL 数据类型文档中找到一些信息 。只需搜索“with timezone”部分。

于 2009-11-01T03:22:36.067 回答