Java向Oracle写入XMLGregorianCalendar时微秒精度丢失问题
问题现象
Java中创建的XMLGregorianCalendar日期对象包含完整微秒(示例:2023-05-24T15:50:07.931456),但通过MyBatis调用存储过程写入Oracle数据库(字段类型为TIMESTAMP(6))后,微秒部分被截断为0(示例:24-MAY-23 15.50.07.931000000 PM),出现精度丢失。
相关代码
Java日期初始化方法
public static XMLGregorianCalendar initializeTime(LocalDateTime localDateTime) { try { XMLGregorianCalendar time= DatatypeFactory.newInstance() .newXMLGregorianCalendar(String.valueOf (LocalDateTime.now().truncatedTo(ChronoUnit.MICROS))); return time; } catch (DatatypeConfigurationException var3) { throw new RuntimeException("Execution time could not created."); } }
MyBatis调用存储过程代码
<select id="add_time" statementType="CALLABLE" parameterType="java.util.Map"> {call dummytest( #{time, mode=IN, jdbcType=TIMESTAMP} )} </select>
Oracle存储过程代码
create or replace procedure dummytest(p_timestamp TIMESTAMP) is begin insert into testtimestamp values (p_timestamp); commit; end ;
精度丢失原因
MyBatis默认类型处理器的精度限制
MyBatis自带的XMLGregorianCalendarTypeHandler依赖XMLGregorianCalendar.getTimeInMillis()方法转换日期,该方法仅返回毫秒级时间戳,微秒信息直接被丢弃,导致转换后的JDBC TIMESTAMP仅保留毫秒精度。XMLGregorianCalendar初始化方式的隐患
通过LocalDateTime转字符串再创建XMLGregorianCalendar的方式,可能存在解析精度问题:DatatypeFactory.newXMLGregorianCalendar(String)在解析字符串时,未必能正确识别并保留微秒部分的精度,导致XMLGregorianCalendar内部未存储完整的微秒值。参数类型精度未明确指定
存储过程参数仅声明为TIMESTAMP,虽Oracle默认TIMESTAMP精度为6,但MyBatis映射中仅指定jdbcType=TIMESTAMP,未明确精度信息,JDBC驱动可能默认按毫秒精度处理参数,进一步截断微秒。
解决办法
1. 自定义XMLGregorianCalendar类型处理器
实现自定义TypeHandler,手动保留微秒信息转换为JDBC TIMESTAMP:
public class XmlGregorianCalendarTimestampTypeHandler extends BaseTypeHandler<XMLGregorianCalendar> { @Override public void setNonNullParameter(PreparedStatement ps, int i, XMLGregorianCalendar parameter, JdbcType jdbcType) throws SQLException { GregorianCalendar gc = parameter.toGregorianCalendar(); Timestamp timestamp = new Timestamp(gc.getTimeInMillis()); // 计算并设置纳秒(微秒转纳秒:1微秒=1000纳秒) timestamp.setNanos(parameter.getMillisecond() * 1000000 + parameter.getMicrosecond() * 1000); ps.setTimestamp(i, timestamp); } // 以下方法根据业务需求实现查询时的转换逻辑 @Override public XMLGregorianCalendar getResult(ResultSet rs, String columnName) throws SQLException { return null; } @Override public XMLGregorianCalendar getResult(ResultSet rs, int columnIndex) throws SQLException { return null; } @Override public XMLGregorianCalendar getResult(CallableStatement cs, int columnIndex) throws SQLException { return null; } }
在MyBatis映射中指定该类型处理器:
#{time, mode=IN, jdbcType=TIMESTAMP, typeHandler=com.yourpackage.XmlGregorianCalendarTimestampTypeHandler}
2. 修正XMLGregorianCalendar初始化逻辑
避免字符串转换,直接通过DatatypeFactory API构建对象,确保微秒被正确存储:
public static XMLGregorianCalendar initializeTime(LocalDateTime localDateTime) { try { LocalDateTime now = LocalDateTime.now().truncatedTo(ChronoUnit.MICROS); DatatypeFactory dtf = DatatypeFactory.newInstance(); XMLGregorianCalendar time = dtf.newXMLGregorianCalendar(); time.setYear(now.getYear()); time.setMonth(now.getMonthValue()); time.setDay(now.getDayOfMonth()); time.setHour(now.getHour()); time.setMinute(now.getMinute()); time.setSecond(now.getSecond()); // 拆分纳秒为毫秒和微秒:XMLGregorianCalendar用millisecond存储前3位,microsecond存储后3位 int nano = now.getNano(); time.setMillisecond(nano / 1000000); time.setMicrosecond((nano % 1000000) / 1000); return time; } catch (DatatypeConfigurationException var3) { throw new RuntimeException("Execution time could not created."); } }
3. 明确参数精度
修改存储过程参数,明确指定TIMESTAMP(6)确保精度匹配:
create or replace procedure dummytest(p_timestamp TIMESTAMP(6)) is begin insert into testtimestamp values (p_timestamp); commit; end ;
内容的提问来源于stack exchange,提问作者Yunus Haznedar

