Mybatis foreach标签使用不当导致异常的原因浅析
异常产生场景及异常信息
上周,由于在mybatis的mapper接口方法中使用实现了map.entry接口的泛型类,同时此方法对应的sql语句也使用了foreach标签,导致出现了异常。如下为异常信息:
org.apache.ibatis.exceptions.persistenceexception: ### error updating database. cause: org.apache.ibatis.reflection.reflectionexception: there is no getter for property named 'key' in 'class java.lang.integer' ### the error may involve org.guojing.test.spring.server.goodsroomnight30daysmapper.insertbatch-inline ### the error occurred while setting parameters ### sql: replace into goods_roomnight_30days (goods_id, checkin_room_night_30days) values (?, ?) , (?, ?), (?, ?), (?, ?) ### cause: org.apache.ibatis.reflection.reflectionexception: there is no getter for property named 'key' in 'class java.lang.integer' 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.session.defaults.defaultsqlsession.insert(defaultsqlsession.java:185) at org.apache.ibatis.binding.mappermethod.execute(mappermethod.java:57) at org.apache.ibatis.binding.mapperproxy.invoke(mapperproxy.java:53) at com.sun.proxy.$proxy4.insertbatch(unknown source) at org.guojing.test.spring.server.goodsroomnight30daystest.test(goodsroomnight30daystest.java:47) at sun.reflect.nativemethodaccessorimpl.invoke0(native method) at sun.reflect.nativemethodaccessorimpl.invoke(nativemethodaccessorimpl.java:57) at sun.reflect.delegatingmethodaccessorimpl.invoke(delegatingmethodaccessorimpl.java:43) at java.lang.reflect.method.invoke(method.java:606) at org.junit.runners.model.frameworkmethod$1.runreflectivecall(frameworkmethod.java:47) at org.junit.internal.runners.model.reflectivecallable.run(reflectivecallable.java:12) at org.junit.runners.model.frameworkmethod.invokeexplosively(frameworkmethod.java:44) at org.junit.internal.runners.statements.invokemethod.evaluate(invokemethod.java:17) at org.junit.internal.runners.statements.runbefores.evaluate(runbefores.java:26) at org.junit.internal.runners.statements.runafters.evaluate(runafters.java:27) at org.junit.runners.parentrunner.runleaf(parentrunner.java:271) at org.junit.runners.blockjunit4classrunner.runchild(blockjunit4classrunner.java:70) at org.junit.runners.blockjunit4classrunner.runchild(blockjunit4classrunner.java:50) at org.junit.runners.parentrunner$3.run(parentrunner.java:238) at org.junit.runners.parentrunner$1.schedule(parentrunner.java:63) at org.junit.runners.parentrunner.runchildren(parentrunner.java:236) at org.junit.runners.parentrunner.access$000(parentrunner.java:53) at org.junit.runners.parentrunner$2.evaluate(parentrunner.java:229) at org.junit.runners.parentrunner.run(parentrunner.java:309) at org.junit.runner.junitcore.run(junitcore.java:160) at com.intellij.junit4.junit4ideatestrunner.startrunnerwithargs(junit4ideatestrunner.java:68) at com.intellij.rt.execution.junit.ideatestrunner$repeater.startrunnerwithargs(ideatestrunner.java:51) at com.intellij.rt.execution.junit.junitstarter.preparestreamsandstart(junitstarter.java:237) 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 'key' in 'class java.lang.integer' 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.session.defaults.defaultsqlsession.update(defaultsqlsession.java:198) ... 29 more
由于本人对mybatis还不是非常的了解,导致用了很长的时间去debug找具体的异常原因。
接下来介绍我将重现异常并分析导致异常的原因。
异常重现
为了重现上面的异常,写了demo。相关代码如下:
数据库表结构:
create table `goods_roomnight_30days` ( `goods_id` bigint(20) not null, `checkin_room_night_30days` int(11) not null default '0' comment '近30天消费间夜', primary key (`goods_id`) ) engine=innodb default charset=utf8 comment='goods的近30天消费间夜'
keyvalue.java 参数类:
public class keyvalue<k, v> implements map.entry<k, v> { private k key; private v value; public keyvalue() { } public keyvalue(k key, v value) { this.key = key; this.value = value; } @override public k getkey() { return key; } @override public v getvalue() { return value; } @override public v setvalue(v value) { v oldvalue = this.value; this.value = value; return oldvalue; } public jsonobject tojsonobject() { return reportjsonobject.newobject().append(string.valueof(key), value); } @override public string tostring() { return tojsonobject().tojsonstring(); } }
dao类goodsroomnight30daysmapper.java
public interface goodsroomnight30daysmapper { int deletebyexample(goodsroomnight30daysexample example); list<goodsroomnight30days> selectbyexample(goodsroomnight30daysexample example); <k, v> int insertbatch(list<keyvalue<k, v>> records); }
mybatis-config.xml文件:
<?xml version="1.0" encoding="utf-8" ?> <!doctype configuration public "-//mybatis.org//dtd config 3.0//en" "http://mybatis.org/dtd/mybatis-3-config.dtd"> <configuration> <settings> <setting name="cacheenabled" value="false"/> </settings> <!-- 和spring整合后 environments配置将废除,交给spring管理--> <environments default="development"> <environment id="development"> <!-- 使用jdbc事务管理--> <transactionmanager type="jdbc" /> <!-- 数据库连接池,整合后一般使用第三方的连接池--> <datasource type="pooled"> <property name="driver" value="com.mysql.jdbc.driver" /> <property name="url" value="jdbc:mysql://localhost:3306/hotel_report?characterencoding=utf-8" /> <property name="username" value="test_user" /> <property name="password" value="user123" /> </datasource> </environment> </environments> <mappers> <mapper resource="mybatis/goodsroomnight30daysmapper.xml"/> </mappers> </configuration>
goodsroomnight30daysmapper.xml文件的主要内容:
<insert id="insertbatch" parametertype="list"> replace into goods_roomnight_30days (goods_id, checkin_room_night_30days) values <foreach collection="list" index="index" item="item" separator=","> (#{item.key}, #{item.value}) <!--<choose>--> <!--<when test="item.value == null">(#{item.key}, 0)</when>--> <!--<when test="item.value != null">(#{item.key}, #{item.value})</when>--> <!--</choose>--> </foreach> </insert>
以上为重现此异常的主要代码,完整代码可在github查看:
重现异常的测试代码 goodsroomnight30daystest.java:
package org.guojing.test.spring.server; import org.apache.ibatis.io.resources; import org.apache.ibatis.session.sqlsession; import org.apache.ibatis.session.sqlsessionfactory; import org.apache.ibatis.session.sqlsessionfactorybuilder; import org.guojing.spring.commons.keyvalue; import org.junit.after; import org.junit.before; import org.junit.test; import java.io.ioexception; import java.io.inputstream; import java.util.arraylist; import java.util.list; /** * created at: 2016-12-24 * * @author guojing */ public class goodsroomnight30daystest { sqlsessionfactory sqlsessionfactory; sqlsession sqlsession; goodsroomnight30daysmapper goodsroomnight30daysmapper; @before public void init() throws ioexception { string resource = "mybatis/mybatis-config.xml"; inputstream inputstream = resources.getresourceasstream(resource); sqlsessionfactory = new sqlsessionfactorybuilder().build(inputstream); sqlsession = sqlsessionfactory.opensession(true); goodsroomnight30daysmapper = sqlsession.getmapper(goodsroomnight30daysmapper.class); } @test public void test() { list<keyvalue<long, integer>> records = new arraylist<>(); records.add(new keyvalue<long, integer>(1725l, 5)); records.add(new keyvalue<long, integer>(1728l, 3)); records.add(new keyvalue<long, integer>(1730l, null)); records.add(new keyvalue<long, integer>(1758l, null)); int deleted = goodsroomnight30daysmapper.deletebyexample(new goodsroomnight30daysexample()); system.out.println("----- deleted row size: " + deleted); int row = goodsroomnight30daysmapper.insertbatch(records); system.out.println("----- affected row: " + row); list<goodsroomnight30days> result = goodsroomnight30daysmapper.selectbyexample(new goodsroomnight30daysexample()); for (goodsroomnight30days item : result) { system.out.println(item.tostring()); } } @after public void after() { if (sqlsession != null) { sqlsession.close(); } } }
卖个关子,大家先不要往下看,想想导致异常的原因(熟练使用foreach标签的同学应该能看出端倪)。
查找异常过程及异常分析
在项目中,由于dao方法的参数类和返回结果类经常会包含一个键和键对应的值,为了避免重复定义类,我定义了一个实现了map.entry接口的keyvalue泛型类,具体请查看上节。
goodsroomnight30daysmapper.insertbatch()
方法就使用了此泛型类。在运行之后就抛出了本文开始提到的异常信息。
看到异常信息后,就把重点放到了是不是mybatis对泛型的支持不够好上。问了下同事(@胜男),同事在自己的机器上试了下,发现没有异常。这就奇怪了。仔细看了下代码,发现不同之处就是我的keyvalue泛型类实现了map.entry接口。 此时还不知道mybatis官网对于foreach标签的说明:
可以将任何可迭代对象(如列表、集合等)和任何的字典或者数组对象传递给foreach作为集合参数。当使用可迭代对象或者数组时,index是当前迭代的次数,item的值是本次迭代获取的元素。当使用字典(或者map.entry对象的集合)时,index是键,item是值。
接下来就是通过debug看看,异常产生的具体原因。于是就先用实现了map.entry接口的keyvalue类的代码进行debug,通过异常日志可以看到异常是在 defaultsqlsession.java:200 行抛出,那么将断点打到 defaultsqlsession.java:197行,然后一步步向下执行,当执行到 foreachsqlnode.java:73行时,终于焕然大悟。先看下debug调用链图:
再看看具体的代码 foreachsqlnode.java:73(此类就是foreach标签对象的node类):
此时具体的异常原因就很明显了,此处的 o 对象的所属的类就是keyvalue类,由于keyvalue类实现了map.entry接口,所以 o instance map.entry 为true,mybatis就把key值赋给了foreach的index属性,而把value值赋给了item属性,此处也就是把值为5的integer对象赋给了item属性。所以 goodsroomnight30daysmapper.xml 中id为 insertbatch 的select标签的item属性对应的对象也就没有 item.key 和 item.value 属性,这就是最终导致异常的原因。
<insert id="insertbatch" parametertype="list"> replace into goods_roomnight_30days (goods_id, checkin_room_night_30days) values <foreach collection="list" index="index" item="item" separator=","> (#{item.key}, #{item.value}) </foreach> </insert>
以上所述是小编给大家介绍的mybatis foreach标签使用不当导致异常的原因浅析,希望对大家有所帮助
上一篇: ASP.NET 跨页面传值方法