背景
最近生产遇到个问题,大致也是遵从墨菲定律吧,你感觉会出问题的地方,可能就真会出问题。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');到此这篇关于oracle中date类型在mybatis中查询时遇到的坑的文章就介绍到这了,更多相关oracle中date类型在mybatis中查询时遇到的坑内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!
发表评论