oracle中date类型在mybatis中查询时遇到的坑
背景
最近生产遇到个问题,大致也是遵从墨菲定律吧,你感觉会出问题的地方,可能就真会出问题。oracle我一直觉得过于复杂,从来没有系统学习过。最近遇到的问题就和date这种类型的列有关系,有个表中的记录大概这样:2024-05-13 00:00:00.000 ,然后类型是date,我还以为这种类型只包含年月日,结果发现其包含了7个字段:世纪、年、月、日、时、分、秒。另外,我还以为这个类型和时区有关系,结果经过这两天的补课,发现这个类型是不包含任何的时区信息的。
简单说下遇到的问题吧,oracle里有一个考勤结果表,主要包含了oa账号、date、考勤结果,之前是有一个数据库定时任务,每天早上6点会调用一个存储过程,存储过程里会往这个表里插入数据。
由于不好调试,且这个存储过程有点bug,我就用xxljob+java重新实现了一下,初版是ai写的,我自己调整了部分代码。
我的java实现大概如下:
为了支持幂等(xxljob重复运行),会检查某个用户在某一天是否已经有记录了,有记录就先删掉,没记录就插入(典型的先删再加)。
结果不记得当时是本地测试不充分还是怎么的,反正就上线了,上线后,xxljob一跑,结果发现存储过程那边写入的记录没删掉,然后重新插入了一条一样的,导致同一个用户同一天有了多条记录,导致app侧报错。
问题代码
数据库表
我们有个这个表:T_ATTENDANCE_RESULT_DAY
核心字段我精简下:
"OA_ACCOUNT" VARCHAR2(20), oa账号
"TERM" DATE类型, 考勤日期
原存储过程写入的数据如下:
SELECT tard.OA_ACCOUNT ,term FROM T_ATTENDANCE_RESULT_DAY tard WHERE tard.OA_ACCOUNT = 'zhangsan' AND tard.TERM = DATE '2026-09-13'
OA_ACCOUNT|TERM |
----------+-----------------------+
zhangsan |2026-09-13 00:00:00.000|
mybatis
注意啊,下面我传的是个LocalDate:
int deleteByBusinessKey(@Param("oaAccount") String oaAccount,
@Param("term") LocalDate term);
下面sql中,term的jdbcType是TIMESTAMP:
<delete id="deleteByBusinessKey">
delete from t_attendance_result_day
where oa_account = #{oaAccount,jdbcType=VARCHAR}
and term = #{term,jdbcType=TIMESTAMP}
</delete>
传值:
@PostMapping(path = "/testDelete")
@DS("attendance")
@Operation(summary = "testDelete", tags = "考勤结果分析处理")
public Message<TAttendanceResultDay> testDelete() {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
LocalDate localDate = LocalDate.parse("2026-09-13", formatter);
int i = tAttendanceResultDayMapper.deleteByBusinessKey("zhangsan", localDate);
System.out.println(i);
return successResponse();
}
再交代下服务器端oracle版本:
SELECT * FROM v$version;
Oracle Database 11g Enterprise Edition Release 11.2.0.4.0 - 64bit Production
java这边的版本:
spring boot 2.7.16
jdk8
<dependency>
<groupId>com.oracle.database.jdbc</groupId>
<artifactId>ojdbc8</artifactId>
<version>23.2.0.0</version>
<scope>compile</scope>
</dependency>
调这个接口试试:
09-13 13:19:33.621 [http-nio-8085-exec-7] DEBUG [42c6843bd13e4a3b] c.h.p.m.T.deleteByBusinessKey ==> Preparing: delete from t_attendance_result_day where oa_account = ? and term = ? [BaseJdbcLogger.java:137]
09-13 13:19:38.440 [http-nio-8085-exec-7] DEBUG [42c6843bd13e4a3b] c.h.p.m.T.deleteByBusinessKey ==> Parameters: zhangsan(String), 2026-09-13(LocalDate) [BaseJdbcLogger.java:137]
09-13 13:19:38.454 [http-nio-8085-exec-7] DEBUG [42c6843bd13e4a3b] c.h.p.m.T.deleteByBusinessKey <== Updates: 0 [BaseJdbcLogger.java:137]
可以发现,影响的行为0行,没删掉。
原因分析
抓包
我其实先抓了个网络包,发现看不到东西,date这种类型的参数,可能不是字符串,所以看不到:

debug
我发现,设置参数主要在这个部分:org.apache.ibatis.executor.SimpleExecutor#doUpdate



这个ParameterHandler是个接口,就包含两方法:
public interface ParameterHandler {
Object getParameterObject();
void setParameters(PreparedStatement ps) throws SQLException;
}
我的项目里就两个实现:
一个是mybatis包里的org.apache.ibatis.scripting.defaults.DefaultParameterHandler
另一个是mybatis-plus里的:com.baomidou.mybatisplus.core.MybatisParameterHandler
我这边走的是mybatis-plus。
方法的实现,主要就是获取sql中,看看需要绑定的参数列表,然后遍历,最终调用preparedStatement中的各种set方法去设置值。


在设置值之前,需要先根据参数的class(如“zhansgan”的class是java.lang.String)和xml中指定的jdbcType,来找到一个handler:

比如上图找到的是StringTypeHandler。然后开始调用handler的方法:
handler.setParameter(ps, i, parameter, jdbcType);
org.apache.ibatis.type.StringTypeHandler这个handler中,设置值,就是调用java.sql.PreparedStatement#setString

最终就会调用到下面oracle驱动的部分:

检查term参数是如何被设置
根据参数类型(LocalDate)和jdbcType(TIMESTAMP),最终的handler为:
org.apache.ibatis.type.LocalDateTypeHandler

这个handler中是调用preparedStatement的setObject方法来设置参数的:

oracle驱动部分
oracle中的setObject方法也是继续调用底层oracle.jdbc.driver.T4CPreparedStatement的setObject方法

然后根据LocalDate类型计算了一个sqlType为93:

这边会把LocalDate转成oracle自己的一个oracle.sql.TIMESTAMP类型:


转换器代码如下:
CONVERTERS.put(new Key(LocalDate.class, TIMESTAMP.class), new JavaToJavaConverter<LocalDate, TIMESTAMP>() {
protected TIMESTAMP convert(LocalDate src, OracleConnection conn, Object srcExtra, Object targetExtra) throws Exception {
return new TIMESTAMP(src); --就是这里
}
});
public TIMESTAMP(LocalDate ld) {
super(toBytes(ld));
}
public static byte[] toBytes(LocalDate ld) {
return ld == null ? null : toBytes(ld.atTime(12, 0, 0)); -- 这里,把传入的LocalDate设置了中午12点!
}
找到问题了,虽然传入的是日期2026-09-13,实际最终变成了:
2026-09-13 12:00:00
那当然是删不掉了。
另外,这里会把这个日期,转换成byte数组:

如2026年,result[0]就是2026/100 + 100 = 120,result[1]就是2026%100 + 100,就是126:

其实拿这个16进制数组,可以去wireshark的抓包中找到对应的数据了:

如何修改
那找到问题了,接下来就修改一下,看起来是传入了LocalDate导致的问题,那我们mapper这里传LocalDateTime吧。
<delete id="deleteByBusinessKey">
delete from t_attendance_result_day
where oa_account = #{oaAccount,jdbcType=VARCHAR}
and term = #{term,jdbcType=TIMESTAMP}
</delete>
int deleteByBusinessKey(@Param("oaAccount") String oaAccount,
@Param("term") LocalDateTime term);
@PostMapping(path = "/testDelete")
@DS("attendance")
@Operation(summary = "testDelete", tags = "考勤结果分析处理")
public Message<TAttendanceResultDay> testDelete() {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
LocalDate localDate = LocalDate.parse("2026-09-13", formatter);
LocalDateTime localDateTime = localDate.atStartOfDay();
int i = tAttendanceResultDayMapper.deleteByBusinessKey("zhangsan", localDateTime);
System.out.println(i);
return successResponse();
}
修改后,重新观察,发现最终在oracle中还是会转换,这次是从LocalDateTime转TIMESTAMP:

现在看着传的没问题了:

结果:
09-13 14:12:46.288 [oaxdb housekeeper] WARN [] com.zaxxer.hikari.pool.HikariPool oaxdb - Thread starvation or clock leap detected (housekeeper delta=50s394ms603µs500ns). [HikariPool.java:788]
09-13 14:12:46.289 [http-nio-8085-exec-1] DEBUG [dbbc18a0db9b4972] c.h.p.m.T.deleteByBusinessKey ==> Parameters: zhangsan(String), 2026-09-13T00:00(LocalDateTime) [BaseJdbcLogger.java:137]
09-13 14:12:46.305 [http-nio-8085-exec-1] DEBUG [dbbc18a0db9b4972] c.h.p.m.T.deleteByBusinessKey <== Updates: 0 [BaseJdbcLogger.java:137]
还是没删除掉!
后面又简单尝试了下别的,还是没解决,只能让ai帮我看了。
ai救场
把这个问题的上下文丢给了ai,我的是glm 5.3,吭哧吭哧干了十几分钟,出结果了。
我好好学习了下它的思路,另外,也感叹下,现在真就是干不过ai了。
ai从我的代码库里找到了数据库连接,账号密码啥的,自己写了个java类,把问题给复现了。
复现后,使用了一个dump函数查看发到数据库服务器端的数据到底是啥。
比如下面的sql,dump(term)就可以查看到oracle中该字段的实际内容:
SELECT dump(term),term FROM T_ATTENDANCE_RESULT_DAY tard WHERE tard.OA_ACCOUNT = 'zhangsan' AND tard.TERM = DATE '2026-09-13'
DUMP(TERM) |TERM |
--------------------------------+-----------------------+
Typ=12 Len=7: 120,126,9,13,1,1,1|2026-09-13 00:00:00.000|
可以看到,这列在数据库侧的实际存储字节是:Typ=12 Len=7: 120,126,9,13,1,1,1
那么,我们这边的mybatis中调用oracle驱动,最终传给数据库服务器的是啥呢?
<select id="selectDump" resultType="java.lang.String">
select dump(#{term,jdbcType=TIMESTAMP}) as termDump from dual
</select>
String selectDump(@Param("term") LocalDateTime term);
@PostMapping(path = "/testDelete")
@DS("attendance")
@Operation(summary = "testDelete", tags = "考勤结果分析处理")
public Message<TAttendanceResultDay> testDelete() {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
LocalDate localDate = LocalDate.parse("2026-09-13", formatter);
LocalDateTime localDateTime = localDate.atStartOfDay();
String string = tAttendanceResultDayMapper.selectDump(localDateTime);
System.out.println(string);
}
运行一下,结果如下:
09-13 14:27:56.407 [http-nio-8085-exec-1] DEBUG [98117dea9df645d3] c.h.p.mapper.TAttendanceResultDayMapper.selectDump ==> Preparing: select dump(?) as termDump from dual [BaseJdbcLogger.java:137]
09-13 14:28:03.425 [http-nio-8085-exec-1] DEBUG [98117dea9df645d3] c.h.p.mapper.TAttendanceResultDayMapper.selectDump ==> Parameters: 2026-09-13T00:00(LocalDateTime) [BaseJdbcLogger.java:137]
09-13 14:28:03.470 [http-nio-8085-exec-1] DEBUG [98117dea9df645d3] c.h.p.mapper.TAttendanceResultDayMapper.selectDump <== Total: 1 [BaseJdbcLogger.java:137]
Typ=180 Len=11: 120,126,9,13,1,1,1,0,0,0,0
这边发现,传过去的内容好像和数据库那边查出来的不一样:
Typ=180 Len=11: 120,126,9,13,1,1,1,0,0,0,0 这边传过去的
vs
Typ=12 Len=7: 120,126,9,13,1,1,1 数据库查出来的
做等值查询的时候,所以就匹配不到了。
如何修改
按照ai建议,修改如下:
使用cast函数:Oracle CAST 是一个显式数据类型转换函数,用于将一个值从一种数据类型转换为另一种兼容的数据类型
我们也写了个xml查询发过去的内容:
<select id="selectDump" resultType="java.lang.String">
select dump(cast(#{term,jdbcType=TIMESTAMP} as date)) as termDump from dual
</select>
运行一下:
09-13 14:49:28.984 [http-nio-8085-exec-2] DEBUG [a9cccd9a74aa435c] c.h.p.mapper.TAttendanceResultDayMapper.selectDump ==> Preparing: select dump(cast(? as date)) as termDump from dual [BaseJdbcLogger.java:137]
09-13 14:49:28.985 [http-nio-8085-exec-2] DEBUG [a9cccd9a74aa435c] c.h.p.mapper.TAttendanceResultDayMapper.selectDump ==> Parameters: 2026-09-13T00:00(LocalDateTime) [BaseJdbcLogger.java:137]
09-13 14:49:28.999 [http-nio-8085-exec-2] DEBUG [a9cccd9a74aa435c] c.h.p.mapper.TAttendanceResultDayMapper.selectDump <== Total: 1 [BaseJdbcLogger.java:137]
Typ=13 Len=8: 234,7,9,13,0,0,0,0
看到内容是:
Typ=13 Len=8: 234,7,9,13,0,0,0,0
和服务端的好像也不同:
SELECT dump(term) FROM T_ATTENDANCE_RESULT_DAY tard WHERE tard.OA_ACCOUNT = 'zhangsan' AND tard.TERM = DATE '2026-09-13'
Typ=12 Len=7: 120,126,9,13,1,1,1
我们实际看看效果:
<delete id="deleteByBusinessKey">
delete from t_attendance_result_day
where oa_account = #{oaAccount,jdbcType=VARCHAR}
and term = cast(#{term,jdbcType=TIMESTAMP} as date)
</delete>
看下面日志,真他么删掉了啊:
09-13 14:54:00.726 [http-nio-8085-exec-1] DEBUG [6fa9425af3924a57] c.h.p.mapper.TAttendanceResultDayMapper.selectDump ==> Preparing: select dump(cast(? as date)) as termDump from dual [BaseJdbcLogger.java:137]
09-13 14:54:00.834 [http-nio-8085-exec-1] DEBUG [6fa9425af3924a57] c.h.p.mapper.TAttendanceResultDayMapper.selectDump ==> Parameters: 2026-09-13T00:00(LocalDateTime) [BaseJdbcLogger.java:137]
09-13 14:54:00.882 [http-nio-8085-exec-1] DEBUG [6fa9425af3924a57] c.h.p.mapper.TAttendanceResultDayMapper.selectDump <== Total: 1 [BaseJdbcLogger.java:137]
Typ=13 Len=8: 234,7,9,13,0,0,0,0
09-13 14:54:00.883 [http-nio-8085-exec-1] DEBUG [6fa9425af3924a57] c.h.p.m.T.deleteByBusinessKey ==> Preparing: delete from t_attendance_result_day where oa_account = ? and term = cast(? as date) [BaseJdbcLogger.java:137]
09-13 14:54:00.884 [http-nio-8085-exec-1] DEBUG [6fa9425af3924a57] c.h.p.m.T.deleteByBusinessKey ==> Parameters: zhangsan(String), 2026-09-13T00:00(LocalDateTime) [BaseJdbcLogger.java:137]
09-13 14:54:00.899 [http-nio-8085-exec-1] DEBUG [6fa9425af3924a57] c.h.p.m.T.deleteByBusinessKey <== Updates: 1 [BaseJdbcLogger.java:137]
那这啥意思呢,我们传过去的是:
Typ=13 Len=8: 234,7,9,13,0,0,0,0
服务端是:
Typ=12 Len=7: 120,126,9,13,1,1,1
这也不相等啊。
ai跟我说:
13是属于外部格式,外部格式(13)是小端直接编码:234,7 → 234+7×256 = 2026 年。
直接传localDateTime为啥不行
大哥又给我说了,是服务端端版本是oracle 11g,这个版本对于目前使用的java驱动来说,太低了。
<dependency>
<groupId>com.oracle.database.jdbc</groupId>
<artifactId>ojdbc8</artifactId>
<version>23.2.0.0</version>
<scope>compile</scope>
</dependency>
支持的版本范围是:
https://www.oracle.com/database/technologies/faq-jdbc.html
最低支持的都是19.x版本,连12.x都不支持,别提11.x了

我当时是由于一个新增需求,才增加了这个java服务读这个oracle库,但为什么选了一个这么高的版本呢,也就几个月前的事,却是一点想不起来了。可能也没想那么多吧。
换回老版本的oracle驱动
换成老版本:
<dependency>
<groupId>com.oracle</groupId>
<artifactId>ojdbc6</artifactId>
<version>11.2.0.3</version>
</dependency>
xml换回来;
<select id="selectDump" resultType="java.lang.String">
select dump(#{term,jdbcType=TIMESTAMP}) as termDump from dual
</select>
数据加回来,重试,直接就报错了,原来是ojdbc6根本不认识LocalDateTime这类类型:

这下,看来我当时选择了高版本的ojdbc8就是这个原因了。只是没考虑到这次遇到的新问题。
换到21.9.0.0
ai大哥让我改回一个稍微老点的版本,21.X,我挑了一个:
<!-- Source: https://mvnrepository.com/artifact/com.oracle.database.jdbc/ojdbc8 -->
<dependency>
<groupId>com.oracle.database.jdbc</groupId>
<artifactId>ojdbc8</artifactId>
<version>21.9.0.0</version>
<scope>compile</scope>
</dependency>
<!-- Source: https://mvnrepository.com/artifact/com.oracle.database.nls/orai18n -->
<dependency>
<groupId>com.oracle.database.nls</groupId>
<artifactId>orai18n</artifactId>
<version>21.9.0.0</version>
<scope>compile</scope>
</dependency>
09-13 15:36:09.340 [http-nio-8085-exec-1] DEBUG [1e8aa02a5a694076] c.h.p.mapper.TAttendanceResultDayMapper.selectDump ==> Preparing: select dump(?) as termDump from dual [BaseJdbcLogger.java:137]
09-13 15:36:10.444 [http-nio-8085-exec-1] DEBUG [1e8aa02a5a694076] c.h.p.mapper.TAttendanceResultDayMapper.selectDump ==> Parameters: 2026-09-13T00:00(LocalDateTime) [BaseJdbcLogger.java:137]
09-13 15:36:10.480 [http-nio-8085-exec-1] DEBUG [1e8aa02a5a694076] c.h.p.mapper.TAttendanceResultDayMapper.selectDump <== Total: 1 [BaseJdbcLogger.java:137]
Typ=180 Len=7: 120,126,9,13,1,1,1
09-13 15:36:11.702 [http-nio-8085-exec-1] DEBUG [1e8aa02a5a694076] c.h.p.m.T.deleteByBusinessKey ==> Preparing: delete from t_attendance_result_day where oa_account = ? and term = ? [BaseJdbcLogger.java:137]
09-13 15:36:13.443 [http-nio-8085-exec-1] DEBUG [1e8aa02a5a694076] c.h.p.m.T.deleteByBusinessKey ==> Parameters: zhangsan(String), 2026-09-13T00:00(LocalDateTime) [BaseJdbcLogger.java:137]
09-13 15:36:13.458 [http-nio-8085-exec-1] DEBUG [1e8aa02a5a694076] c.h.p.m.T.deleteByBusinessKey <== Updates: 1 [BaseJdbcLogger.java:137]
换回21.9.0.0版本后,发现dump内容变成了:
Typ=180 Len=7: 120,126,9,13,1,1,1
和最早的23.x版本,确实不同了:
Typ=180 Len=11: 120,126,9,13,1,1,1,0,0,0,0
无法查询问题--最终可行的方案
换成21.9.0.0版本
下面这样就可以:
int deleteByBusinessKey(@Param("oaAccount") String oaAccount,
@Param("term") LocalDateTime term);
<delete id="deleteByBusinessKey">
delete from t_attendance_result_day
where oa_account = #{oaAccount,jdbcType=VARCHAR}
and term = #{term,jdbcType=TIMESTAMP}
</delete>
保持23.x版本
方法1
用cast方式,
int deleteByBusinessKey(@Param("oaAccount") String oaAccount,
@Param("term") LocalDateTime term);
<delete id="deleteByBusinessKey">
delete from t_attendance_result_day
where oa_account = #{oaAccount,jdbcType=VARCHAR}
and term = cast(#{term,jdbcType=TIMESTAMP} as date)
</delete>
方法2
<delete id="deleteByBusinessKey">
delete from t_attendance_result_day
where oa_account = #{oaAccount,jdbcType=VARCHAR}
and term = to_date(#{term,jdbcType=VARCHAR},'yyyy-MM-dd hh24:mi:ss')
</delete>
int deleteByBusinessKey(@Param("oaAccount") String oaAccount,
@Param("term") String term);
这个就传字符串,最保险。再结合版本换成21.9,应该是最稳妥的。
方法3
动态拼sql:
<delete id="deleteByBusinessKey">
delete from t_attendance_result_day
where oa_account = #{oaAccount,jdbcType=VARCHAR}
and term = timestamp '${term}'
</delete>
int deleteByBusinessKey(@Param("oaAccount") String oaAccount,
@Param("term") String term);
int i = tAttendanceResultDayMapper.deleteByBusinessKey("zhangsan", "2026-09-13 00:00:00");
下面这样写不行,必须动态拼sql才行:
<delete id="deleteByBusinessKey">
delete from t_attendance_result_day
where oa_account = #{oaAccount,jdbcType=VARCHAR}
and term = timestamp #{term}
</delete>
插入问题
其实insert也会有问题,之前这个term字段都是定义成LocalDate的。插入的时候,比如赋值为:
TAttendanceResultDay day = new TAttendanceResultDay();
day.setTerm(LocalDate.now());
day.setOaAccount("demo");
tAttendanceResultDayMapper.insert(day);
就像前面说的那样,LocalDate会被oracle驱动转换为它自己的TIMESTAMP,转的时候,就变成了这一天的12点。
2026-09-13 12:00:00.000
把字段类型弄成LocalDateTime就行了。
一个坑点
TAttendanceResultDay day = new TAttendanceResultDay();
LocalDateTime now = LocalDateTime.now();
day.setTerm(now);
day.setOaAccount("demo");
day.setBadge("001140");
tAttendanceResultDayMapper.insert(day);
执行完上面的插入后,比如now是:2026-09-13T16:19:16.389,但最终在数据库是这样的: 2026-09-13 16:19:16.000,没有最后的毫秒部分。
String string = tAttendanceResultDayMapper.selectDump(now);
System.out.println(string); -- Typ=180 Len=11: 120,126,9,13,17,20,17,23,47,171,64
但是这边等值匹配的时候,是带了毫秒的,依然会删不掉:
int i = tAttendanceResultDayMapper.deleteByBusinessKey("demo", now, "001140");
System.out.println(i);
总结
看来看去,还是字符串格式最好,少了好多坑。
用这种算了:
<delete id="deleteByBusinessKey">
delete from t_attendance_result_day
where oa_account = #{oaAccount,jdbcType=VARCHAR}
and term = to_date(#{term,jdbcType=VARCHAR},'yyyy-MM-dd hh24:mi:ss')
</delete>
int deleteByBusinessKey(@Param("oaAccount") String oaAccount,
@Param("term") String term);
参考
插入示例:
INSERT INTO your_table_name (date_column) VALUES (TO_DATE('2024-05-13', 'YYYY-MM-DD'));
或者:
INSERT INTO your_table_name (date_column) VALUES (DATE '2024-05-13');
查询时怎么查:
select * from your_table_name where to_char(date_column,'yyyy-mm-dd') = '2024-05-13'; 这个走不了索引
或者
select * from your_table_name where date_column = date '2024-05-13';
或者
select * from your_table_name where date_column = timestamp '2024-05-13 00:00:00';
或者
select * from your_table_name where date_column = to_date('2024-05-13','yyyy-mm-dd')
select * from your_table_name where date_column >= to_date('2024-05-13','yyyy-mm-dd') and update_time < to_date('2024-05-14','yyyy-mm-dd');

浙公网安备 33010602011771号