MyBatis 参数重复打印的bug

现象

最近有个需求,需要在mybatis对数据库进行写入操作的时候,根据条件对对象中的某个值进行置空,然后再进行写入,这样数据库中的值就会为空了。

根据网上查看的资料,选择在 StatementHandler 类执行 update 的时候进行对参数的拦截,

修改参数完毕后,再调用 statementHandler.getParameterHandler().setParameters 方法将修改后的值重新 set 进去,

此时出现了问题,发现mybatis在控制台log中打印出来的参数竟然多了一份,下面是临摹的情况,没有实际用update ,而是拦截的query方法,简化了一下逻辑:

这里我调用了3次 setParameters ,加上mybatis本身的一次,输出了4个 Parameters

实际上我的xml中只接收了一个参数,并且虽然log多打印了一些参数,实际对我的结果并无影响。

为什么会发生这个问题

问题就出在 StatementHandler.getParameterHandler().setParameters 这个方法上,这里是对本次数据库操作的参数进行赋值,

这过程中有一个 typeHandler.setParameter(ps, i + 1, value, jdbcType) 操作,

这里会根据相应的类型处理器,进行赋值,这个过程如下,typeHandler进行赋值操作,

这时候会调用 jdbcPreparedStatement.setXxx 方法,但是 mybatis对 PreparedStatement 做了一个拦截,

这个拦截就是日志记录类 PreparedStatementLogger ,当调用setXxx方法时,

Logger类会调用 setColumn 方法,这个setColumn方法就是记录本次入参情况,

最终调用 PreparedStatement.executeXXX方法时,本次代理将会根据条件决定要不要打印出参数等log,

而这个参数log,就是前面我们提到的setColumn方法中存入的 columnValues 属性,

可以看到,这个属性使用的是 ArrayList ,这也就说明了为什么会重复打印参数log出来,这意味着当你多次调用 StatementHandler.getParameterHandler().setParameters 这个方法时,columnValues 不会清除之前记录的参数,并且继续保存你这次重新set的参数进来。

如何解决

如果mybatis不使用ArrayList存值是否就可以避免这个问题,并且columnNamescolumnValues这两个值仅仅在打印log的时候使用,并没有在其他地方有使用到,

columnMap 刚好起到了,就算多次调用StatementHandler.getParameterHandler().setParameters方法,但是因为Map有过滤重复key的作用,然后使用columnMap中记录的值就可以防止参数重复打印的问题,

于是我给mybatis提了一个pr,借此来修复这个问题, pr链接:https://github.com/mybatis/mybatis-3/pull/3110

我的思路很简单,去除columnNamescolumnValues,仅保留columnMap,并且将columnMap改为LinkedHashMap类型,以此保证参数的顺序,经过测试这样做并没有发现什么问题,且保证了参数的正确输出。

不过很遗憾我的pr没有被合并,被关闭了,对方给的理由是不保证在拦截器中做出的一些操作对mybatis的运行产生一些的副作用,且给出友好提示,是否有其他的方式来解决我的需求问题。

重新判断问题

我们重新思考一下这个问题,问题出现在StatementHandler.getParameterHandler().setParameters 这里,我对参数重新赋值会导致这个问题,

那么我为何要重新赋值?有没有办法不调用这个StatementHandler.getParameterHandler().setParameters 方法?

我需要重新赋值的原因是因为在拦截StatementHandlerupdatequery方法时,mybatis自身已经调用过setParameters方法,

此时如果我不重新调用一下,单纯的修改parameterObject 自身,那么PreparedStatement.executeXXX设置的参数其实还是上次的,并不会因为我修改了parameterObject 而变化,

所以根据提示,我们有没有办法在mybatis自身执行setParameters前进行对parameterObject 的修改,这样我们在执行过程中,由mybatis来做这个赋值的事情,log就只会打印一次了。

最终解决

那么mybatis是何时自己进行setParameters的?答案在StatementHandler.parameterize中,

是的,它会在update或者query方法执行前,对参数进行处理,所以我们应当拦截这一步的操作,在这一步对参数进行处理,

修改后的拦截器,可以看到这里我们对StatementHandler.parameterize方法进行拦截处理,并且修改参数,此时log中输出的就是我们最后一次修改的参数。

当然mybatis还提供了其他的拦截点,例如不拦截StatementHandler类,我们直接到源头ParameterHandler.setParameters,拦截设置参数方法,

在这里,我们修改他的参数也是可以的,如下图:

最终结论

可以看到,我们只要不在拦截器中调用setParameters方法,就不会触发log的重复打印,因为mybatis的log记录类,使用ArrayList记录每次的setXX入参, 因此选好时机做相应的处理,就不会出现问题,在合适的拦截点做相应的事情,

MyBatis的参数记录可能也没有考虑过重复调用的问题,或者也许有其他的考量,总之我们了解这个问题的原因,并且做相应的规避即可。

演示与复现问题的demo都在:https://github.com/qiaomengnan16/mybatis-log-bug,欢迎指正。

相关推荐
ClouGence4 天前
Oracle 数据同步为什么会出现数据不一致?长事务是常被忽略的原因
数据库·后端·oracle
ClouGence10 天前
Oracle CDC 架构优化:从主库直连到 DataGuard 备库同步
数据库·后端·oracle
曹牧11 天前
Oracle EXPLAIN PLAN
数据库·oracle
贤时间11 天前
codex 助力oracle ebs 开发
数据库·oracle
秉承初心11 天前
PostgreSQL 数据性能瓶颈突破实战
数据库·postgresql·oracle
敲个大西瓜11 天前
mybatis拦截器插件实现数据库字段加解密
mybatis
Curvatureflight11 天前
MySQL 深分页越来越慢?从 LIMIT OFFSET 改成游标分页
数据库·oracle
XZ-07000111 天前
MySQL事务
数据库·mysql·oracle
tiancaijiben11 天前
阿里云函数计算FC如何实现网站的定时任务与自动化
数据库·oracle·dba