Mybatis foreach標籤使用不當致使異常的緣由探究

Mybatis foreach標籤使用不當致使異常的緣由探究

異常產生場景及異常信息

上週,因爲Mybatis的Mapper接口方法參數使用實現了Map.Entry接口的泛型類,同時此方法對應的sql語句也使用了foreach標籤,致使出現了異常。以下爲異常信息:html

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還不是很是的瞭解,本能的懷疑是Mybatis對泛型的支持不夠好致使。java

接下來介紹我是如何重現異常並分析致使異常的緣由。mysql

異常重現

爲了重現上面的異常,寫了demo。相關代碼以下:git

數據庫表結構:github

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 參數類:spring

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.javasql

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文件的主要內容:apache

<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查看:https://github.com/misterzhou/java-demo/tree/master/test-spring/spring-server數組

重現異常的測試代碼 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對泛型的支持不夠好上,因而問了下同事(@勝南),同事寫了個Demo在本身的機器上試了下,發現沒有異常。這就奇怪了,仔細看了下代碼,不一樣之處就是個人KeyValue泛型類實現了Map.Entry接口。此時還不知道mybatis官網對於foreach標籤的說明(連接地址:http://www.mybatis.org/mybatis-3/zh/dynamic-sql.html#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類): ForEachSqlNode.java:73

此時具體的異常緣由就很明顯了,此處 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>
相關文章
相關標籤/搜索