org.apache.commons.lang3.tuple.Pair 作为更新参数,XML 中的 Sql 取不到值、报错
项目用的 Mybatis,今天改一个需求,落地实现是批量更新,且只需要根据主键(id)来更新一个字段(name)。
于是,没有犹豫,像下面这样设计了数据结构:
- 既然是批量更新,那外层肯定是 List
- List 中每个元素,只包含 id & name,于是,选择了用 org.apache.commons.lang3.tuple.Pair 来封装数据(就是不想自己再写一个 DO 或者 VO 或者 MO)
- 最终的数据结构是:List<Pair<Integer, String>>
XML 中的 Sql 语句,很简单,如下:
<update id="updateByApacheCommonsPair"> <foreach collection="list" separator=";" item="x"> UPDATE employee SET name = #{x.right} WHERE id = #{x.left} </foreach> </update>
提示:XML 中的 Sql 引入了一个名为 x 的变量(后面会用到)
一切顺利,但测试时,报错如下:
org.apache.ibatis.exceptions.PersistenceException: ### Error updating database. Cause: org.apache.ibatis.reflection.ReflectionException: There is no getter for property named 'right' in 'class java.lang.String' ### The error may involve defaultParameterMap ### The error occurred while setting parameters ### SQL: UPDATE employee SET name = ? WHERE id = ? ### Cause: org.apache.ibatis.reflection.ReflectionException: There is no getter for property named 'right' in 'class java.lang.String' at org.apache.ibatis.exceptions.ExceptionFactory.wrapException(ExceptionFactory.java:30) at org.apache.ibatis.session.defaults.DefaultSqlSession.update(DefaultSqlSession.java:200) at org.apache.ibatis.binding.MapperMethod.execute(MapperMethod.java:62) at org.apache.ibatis.binding.MapperProxy.invoke(MapperProxy.java:53) at com.sun.proxy.$Proxy4.updateByApacheCommonsPair(Unknown Source) at com.atguigu.mybatis.test.MyBatisTest.testUpdateByApacheCommonsPair(MyBatisTest.java:42) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:498) at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50) at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12) at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47) at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17) at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325) at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:78) at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:57) at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290) at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71) at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288) at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58) at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268) at org.junit.runners.ParentRunner.run(ParentRunner.java:363) at org.junit.runner.JUnitCore.run(JUnitCore.java:137) at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:68) at com.intellij.rt.execution.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:47) at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:242) at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:70) Caused by: org.apache.ibatis.reflection.ReflectionException: There is no getter for property named 'right' in 'class java.lang.String' at org.apache.ibatis.reflection.Reflector.getGetInvoker(Reflector.java:409) at org.apache.ibatis.reflection.MetaClass.getGetInvoker(MetaClass.java:164) at org.apache.ibatis.reflection.wrapper.BeanWrapper.getBeanProperty(BeanWrapper.java:162) at org.apache.ibatis.reflection.wrapper.BeanWrapper.get(BeanWrapper.java:49) at org.apache.ibatis.reflection.MetaObject.getValue(MetaObject.java:122) at org.apache.ibatis.reflection.MetaObject.getValue(MetaObject.java:119) at org.apache.ibatis.mapping.BoundSql.getAdditionalParameter(BoundSql.java:75) at org.apache.ibatis.scripting.defaults.DefaultParameterHandler.setParameters(DefaultParameterHandler.java:72) at org.apache.ibatis.executor.statement.PreparedStatementHandler.parameterize(PreparedStatementHandler.java:93) at org.apache.ibatis.executor.statement.RoutingStatementHandler.parameterize(RoutingStatementHandler.java:64) at org.apache.ibatis.executor.SimpleExecutor.prepareStatement(SimpleExecutor.java:86) at org.apache.ibatis.executor.SimpleExecutor.doUpdate(SimpleExecutor.java:49) at org.apache.ibatis.executor.BaseExecutor.update(BaseExecutor.java:117) at org.apache.ibatis.executor.CachingExecutor.update(CachingExecutor.java:76) at org.apache.ibatis.session.defaults.DefaultSqlSession.update(DefaultSqlSession.java:198) ... 26 more
问题已经出了,那就解决吧。
报错中的提示信息还是比较容易阅读:java.lang.String 类中没有 right 字段的 getter 方法
奇怪,怎么会报这样的错:
- java.lang.String 类中确实没有 right 字段
- right 字段是 Pair 中的(因为我用的就是 Pair)
- 它们两者搅在一起了?
猜测了一些原因,不过都不对,最后,没办法了,只有自己写了一个 CYHPair (也是一个包含两个字段的POJO)来实现,结果,又是可以的。
所以,接下来就有思路了:比较自己写的 CYHPair 和 org.apache.commons.lang3.tuple.Pair 在哪些地方有所不同。
最后发现(实验了很长时间),一个比较大的不同之处在于,org.apache.commons.lang3.tuple.Pair 实现了 Map.Entry 接口(自己写的 CYHPair 没有),这会不会有影响?
最终的结论证明:就是因为实现了 Map.Entry 所以导致了后面取不到值,所以报错如上。
可是,这又是为什么呢?于是,继续源码调试。
Demo 代码地址:https://github.com/cyhbyw/mybatis_atguigu
Demo 工程名称:MyBatis_CYH_test_MapEntry
第一:先来调试正常的Case(即基于 CYHPair 来做更新操作的)
01. 可以看到,Line#73 判断了是否属于 Map.Entry;由于我的 CYHPair 没有实现它,所以,代码进入 else 部分;注意这里的第二个参数是 o 本身,而 o 是 CYHPair 的一个实例
02. 从 Line#102 可以看到,将 o 这个 CYHPair 对象绑定给了 x (提示:x 是 Sql 中的变量)
简单结论:x 这个变量是 CYHPair 类型的!
第二步:接下来调试不正常的(即基于 org.apache.commons.lang3.tuple.Pair 来做更新操作的)
01. 还是 Line#73 行的判断,不过这一次,判断成立,于是,代码走到 Line#77 行;注意第二个参数是 mapEntry.getValue(),而这个值是字符串 ApacheCommonsPair;
02. 绑定的时候,就将 ApacheCommonsPair 这个字符串值绑定给了变量 x
03. 所以现在容易理解它为什么会报错 “java.lang.String 类中没有 right 字段的 getter 方法” 了,因为就是 02 步中将字符串 ApacheCommonsPair 绑定给了 x 呀,那就肯定没有 right 字段也没有对应的 getter 方法啦~~