You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用java.sql.SQLData写入时Timestamp被时区偏移移位的问题排查

问题:使用java.sql.SQLData写入数据时Timestamp时区偏移问题

问题问询

有人能解答为何使用java.sql.SQLData写入数据时,Timestamp会被偏移N小时(N为时区偏移量)吗?

背景

基于Hibernate 6和SpringBoot 3的应用,需要调用接收自定义类型列表的存储过程。尝试Hibernate 6/5实现未果,转而使用Spring的JdbcTemplate工具。

当前状态

数据可持久化,但java.sql.Timestamp存入数据库时发生时间移位:

  • 原Instant为10:00:00Z(正确)
  • 转换为Timestamp后,toString()显示12:00:00(本地时区+2,属于正常展示),toGMTString()显示正确的10:00:00 GMT
  • 通过SQLOutput.writeTimestamp()写入时,数据源代理日志显示移位后的时间被存入,推测为Oracle驱动导致

临时解决方案

通过反向偏移Timestamp抵消驱动的异常处理,但不认可该方案,寻求问题原因及正规预防方法。

实现代码

存储过程调用代码

jdbcTemplate.call(con -> {
        CallableStatement callableStatement = con.prepareCall(PROCEDURE_NAME);
        OracleConnection oracleConnection = con.unwrap(OracleConnection.class);

        Array oracleArray = oracleConnection.createOracleArray(PROCEDURE_ARRAY_TYPE_NAME,
                inputArray);

        callableStatement.setArray(1, oracleArray);
        return callableStatement;

    }, Collections.singletonList(new SqlParameter(Types.STRUCT)));

自定义SQLData类型

public class TTT implements SQLData {
        private static final String SQL_TYPE = "%s.%s".formatted("someSchema", "someType");

        private UUID id;
        private Timestamp calculatedTimestamp;

        public TTT() {
        }

        public TTT(SomeEntity someEntity) {
            this.id = someEntity.getId();
            this.calculatedTimestamp = Timestamp.from(someEntity.getCalculatedTimestamp());
            
        }

        @Override
        public String getSQLTypeName() {
            return SQL_TYPE;
        }

        @Override
        public void readSQL(SQLInput stream,
                            String typeName) {
            throw new UnsupportedOperationException("Not implemented yet");
        }

        @Override
        public void writeSQL(SQLOutput stream) throws SQLException {
            stream.writeBytes(UuidUtil.uuidToByteArray(id));
            stream.writeTimestamp(calculatedTimestamp);
            
            // 尝试过但失败的写法
//            stream.writeObject(calculatedTimestamp, OracleType.TIMESTAMP_WITH_LOCAL_TIME_ZONE);
//            stream.writeObject(calculatedTimestamp, OracleType.TIMESTAMP);
        }
    }

临时修复代码

private Timestamp nastyTimestampHackConversion(Instant instant) {
        if (instant == null) {
            return null;
        }

        long offsetSecondsDiff = ZoneId.systemDefault().getRules().getOffset(instant).getTotalSeconds();
        Instant fixedInstant = instant.plus(-1*offsetSecondsDiff, ChronoUnit.SECONDS);
        return Timestamp.from(fixedInstant);
    }

解答

问题原因

  1. Oracle驱动时区处理逻辑:Oracle JDBC驱动默认会将JVM本地时区的时间转换为数据库会话时区时间。若JVM时区与数据库会话时区不一致,或驱动错误地将Timestamp的本地展示时间当作实际值处理,就会引发偏移。
  2. java.sql.Timestamp特性限制:Timestamp内部存储UTC时间戳,但toString()会以本地时区展示;Oracle驱动的SQLOutput.writeTimestamp()实现可能误将本地展示值作为写入依据,而非内部UTC时间戳。
  3. SQLData与Oracle自定义类型适配问题:使用SQLData写入自定义类型时,驱动的时间转换规则和直接使用PreparedStatement不同,缺失了正确的时区校准逻辑。

正规解决方案

方案1:统一数据库会话时区

获取连接后,将数据库会话时区设置为UTC(或业务统一时区),避免驱动自动转换时出现偏移:

OracleConnection oracleConnection = con.unwrap(OracleConnection.class);
// 设置会话时区为UTC
oracleConnection.setSessionTimeZone("UTC");

方案2:使用Oracle带时区的时间类型写入

改用Oracle驱动提供的TIMESTAMP WITH TIME ZONE类型,直接传入带明确时区的时间对象,跳过驱动的自动转换:

@Override
public void writeSQL(SQLOutput stream) throws SQLException {
    stream.writeBytes(UuidUtil.uuidToByteArray(id));
    // 将Instant转换为Oracle带时区的时间对象
    OracleTimestampWithTimeZone tsWithTz = OracleTimestampWithTimeZone.from(
        someEntity.getCalculatedTimestamp(), TimeZone.getTimeZone("UTC")
    );
    stream.writeObject(tsWithTz);
}

需确保数据库自定义类型的对应字段为TIMESTAMP WITH TIME ZONE类型。

方案3:使用Java 8+时间API替代Timestamp

弃用过时的Timestamp,改用OffsetDateTime明确控制时区,配合Oracle驱动的类型标识写入:

// TTT类中替换字段类型
private OffsetDateTime calculatedTimestamp;

public TTT(SomeEntity someEntity) {
    this.id = someEntity.getId();
    this.calculatedTimestamp = someEntity.getCalculatedTimestamp().atOffset(ZoneOffset.UTC);
}

@Override
public void writeSQL(SQLOutput stream) throws SQLException {
    stream.writeBytes(UuidUtil.uuidToByteArray(id));
    stream.writeObject(calculatedTimestamp, OracleType.TIMESTAMP_WITH_TIME_ZONE);
}

方案4:配置JDBC驱动时区参数

在JDBC URL中添加参数,强制驱动使用UTC处理时间:

jdbc:oracle:thin:@//your-db-host:port/service_name?oracle.jdbc.timezoneAsRegion=false&oracle.jdbc.defaultTimezone=UTC

oracle.jdbc.timezoneAsRegion=false禁用时区区域名称解析,oracle.jdbc.defaultTimezone=UTC设置驱动默认时区为UTC。


内容的提问来源于stack exchange,提问作者Martin Mucha

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.17 20:22:13