现象
最近有个需求,需要在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进行赋值操作,
这时候会调用 jdbc
的 PreparedStatement.setXxx
方法,但是 mybatis对 PreparedStatement
做了一个拦截,
这个拦截就是日志记录类 PreparedStatementLogger
,当调用setXxx方法时,
Logger类会调用 setColumn
方法,这个setColumn方法就是记录本次入参情况,
最终调用 PreparedStatement.executeXXX
方法时,本次代理将会根据条件决定要不要打印出参数等log,
而这个参数log,就是前面我们提到的setColumn方法中存入的 columnValues
属性,
可以看到,这个属性使用的是 ArrayList
,这也就说明了为什么会重复打印参数log出来,这意味着当你多次调用 StatementHandler.getParameterHandler().setParameters
这个方法时,columnValues
不会清除之前记录的参数,并且继续保存你这次重新set的参数进来。
如何解决
如果mybatis不使用ArrayList存值是否就可以避免这个问题,并且columnNames
和columnValues
这两个值仅仅在打印log的时候使用,并没有在其他地方有使用到,
而columnMap
刚好起到了,就算多次调用StatementHandler.getParameterHandler().setParameters
方法,但是因为Map有过滤重复key的作用,然后使用columnMap
中记录的值就可以防止参数重复打印的问题,
于是我给mybatis提了一个pr,借此来修复这个问题, pr链接:https://github.com/mybatis/mybatis-3/pull/3110
我的思路很简单,去除columnNames
和columnValues
,仅保留columnMap
,并且将columnMap
改为LinkedHashMap
类型,以此保证参数的顺序,经过测试这样做并没有发现什么问题,且保证了参数的正确输出。
不过很遗憾我的pr没有被合并,被关闭了,对方给的理由是不保证在拦截器中做出的一些操作对mybatis的运行产生一些的副作用,且给出友好提示,是否有其他的方式来解决我的需求问题。
重新判断问题
我们重新思考一下这个问题,问题出现在StatementHandler.getParameterHandler().setParameters
这里,我对参数重新赋值会导致这个问题,
那么我为何要重新赋值?有没有办法不调用这个StatementHandler.getParameterHandler().setParameters
方法?
我需要重新赋值的原因是因为在拦截StatementHandler
的update
或query
方法时,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,欢迎指正。