Mybatis3.3.x技術內幕(十五):Mybatis之foreach批量insert,返回主鍵id列表(修復Mybatis返回null的bug)

Mybatis在執行批量插入時,若是使用的是for循環逐一插入,那麼能夠正確返回主鍵id。若是使用動態sql的foreach循環,那麼返回的主鍵id列表,可能爲null,這讓不少人感到困惑;本文將分析問題產生的緣由,並修復返回主鍵id爲null的問題。該問題在開源中國社區,以及網絡上,已經有不少人遇到併發帖諮詢,彷佛都沒有獲得指望的解決結果。今天,我將帶領你們,分析並解決該問題,讓foreach批量插入,返回正確的id列表。java

<insert id="insertStudents" useGeneratedKeys="true" keyProperty="studId" parameterType="java.util.ArrayList">
		INSERT INTO
		STUDENTS(STUD_ID, NAME, EMAIL, DOB, PHONE)
		VALUES
	<foreach collection="list" item="item" index="index" separator=","> 
        	(#{item.studId},#{item.name},#{item.email},#{item.dob}, #{item.phone}) 
    	</foreach> 
	</insert>

以上即是Mybatis的foreach循環,其要生成的sql語句是:insert into students(stud_id, name) values(?, ?),(?, ?), (?, ?); 相似這樣的批量插入。程序員

Mybatis是對Jdbc的封裝,咱們來看看,Jdbc是否支持上述形式的批量插入,並返回主鍵id列表的。sql

PreparedStatement pstm = conn.prepareStatement("insert into students(name, email) values(?, ?), (?, ?), (?, ?)",
				Statement.RETURN_GENERATED_KEYS);

pstm.setString(1, "name1");
pstm.setString(2, "email1");


pstm.setString(3, "name2");
pstm.setString(4, "email2");
		
pstm.setString(5, "name2");
pstm.setString(6, "email2");

pstm.addBatch();
pstm.executeBatch();

ResultSet rs = pstm.getGeneratedKeys();
while (rs.next()) {
	Object value = rs.getObject(1);
	System.out.println(value);
}

Output:數據庫

248
249
250

好了,事實證實,Jdbc是支持上述批量插入,並能正確返回id列表的。Jdbc都支持,若是Mybatis卻不支持,有點說不過去。apache

1. Mapper.xml中keyProperty和parameterType屬性之間的關係(很重要)數組

useGeneratedKeys="true" keyProperty="studId" parameterType="Student"

上述xml配置,含義爲,屬性studId是參數類型Student對象的主鍵屬性。毫無疑問,Student對象中有studId屬性。網絡

useGeneratedKeys="true" keyProperty="studId" parameterType="java.util.ArrayList"

那這個如何解釋呢?ArrayList有studId屬性嗎?固然沒有了。其正確含義爲:ArrayList集合中的元素的studId屬性。session

因此,keyProperty和parameterType之間的關係,有時是直接關係,有時是間接關係。明白這個道理以後,咱們就能夠開始進一步閱讀源碼了。數據結構

2. Mybatis對parameter object的解析mybatis

org.apache.ibatis.executor.keygen.Jdbc3KeyGenerator.java源碼(只保留了重點源碼)

@Override
  public void processAfter(Executor executor, MappedStatement ms, Statement stmt, Object parameter) {
    processBatch(ms, stmt, getParameters(parameter));
  }

  public void processBatch(MappedStatement ms, Statement stmt, Collection<Object> parameters) {
    ResultSet rs = null;
    try {
      rs = stmt.getGeneratedKeys();
        // 迭代出來的對象parameter,必定要具有keyProperty屬性
        for (Object parameter : parameters) {
          metaParam.setValue(keyProperties, value);
        }
      }
    }
  }

  private Collection<Object> getParameters(Object parameter) {
    Collection<Object> parameters = null;
    if (parameter instanceof Collection) {
      // 集合
      parameters = (Collection) parameter;
    } else if (parameter instanceof Map) {
      // map
      Map parameterMap = (Map) parameter;
      if (parameterMap.containsKey("collection")) {
        parameters = (Collection) parameterMap.get("collection");
      } else if (parameterMap.containsKey("list")) {
        parameters = (List) parameterMap.get("list");
      } else if (parameterMap.containsKey("array")) {
        parameters = Arrays.asList((Object[]) parameterMap.get("array"));
      }
    }
    if (parameters == null) {
      parameters = new ArrayList<Object>();
      parameters.add(parameter);
    }
    return parameters;
  }

上面這段代碼,很是關鍵且重要,特別是我作了註釋的地方,for(Object parameter : parameters)循環,表示parameters必定是一個集合,若是傳遞的是Student對象,那麼Mybatis會將其封裝到List<Student>中,而後再進行迭代操做。因而,迭代出來的parameter就是Student對象,就具有了keyProperty指定的屬性了,好比studId屬性。

若是傳遞的是一個List<Student>呢?

org.apache.ibatis.session.defaults.DefaultSqlSession.wrapCollection(Object)源碼。

executor.update(ms, wrapCollection(parameter));
// ...
  private Object wrapCollection(final Object object) {
    // 若是是集合,再度包裝爲Map對象
    if (object instanceof Collection) {
      StrictMap<Object> map = new StrictMap<Object>();
      map.put("collection", object);
      if (object instanceof List) {
        map.put("list", object);
      }
      return map;
    } else if (object != null && object.getClass().isArray()) {
      // 數組
      StrictMap<Object> map = new StrictMap<Object>();
      map.put("array", object);
      return map;
    }
    return object;
  }

上面這段代碼也很是重要,若是傳遞的是List<Student>,那麼,將包裝爲一個Map<String, Collection>對象。

因而,List<Student>形式的parameter object就變成了下面這個樣子,一個Map<String, List<Student>>對象,Map的size()爲2,key分別爲「collection」和「list」。下面會常常用到這個Map<String, List<Student>>對象,因此,要記住其數據結構。

{
    collection=[
        com.mybatis3.domain.Student@2d2ffcb7,
        com.mybatis3.domain.Student@762ef0ea
    ],
    list=[
        com.mybatis3.domain.Student@2d2ffcb7,
        com.mybatis3.domain.Student@762ef0ea
    ]
}

所以,Mybatis將集合類參數對象,包裝成上面的一個Map<String, List<Student>>結構了。明白了數據的組織結構,就能夠進行下一步的分析了。

3. SimpleExecutor和ReuseExecutor能夠正確返回foreach批量插入後的id列表的原理

還記得如何配置Executor嗎?

<setting name="defaultExecutorType" value="SIMPLE" />

既然集合參數,已經被包裝成了Map<String, List<Student>>對象,固然就沒法使用for(Object parameter : parameters)來迭代Map<String, List<Student>>了,咱們看看SimpleExecutor和ReuseExecutor是如何作到的。

private Collection<Object> getParameters(Object parameter) {
    Collection<Object> parameters = null;
    if (parameter instanceof Collection) {
      parameters = (Collection) parameter;
    } else if (parameter instanceof Map) {
      Map parameterMap = (Map) parameter;
      if (parameterMap.containsKey("collection")) {
        // 返回map中key=collection的value
        parameters = (Collection) parameterMap.get("collection");
      } else if (parameterMap.containsKey("list")) {
        // 返回map中key=list的value
        parameters = (List) parameterMap.get("list");
      } else if (parameterMap.containsKey("array")) {
        parameters = Arrays.asList((Object[]) parameterMap.get("array"));
      }
    }
    if (parameters == null) {
      parameters = new ArrayList<Object>();
      parameters.add(parameter);
    }
    return parameters;
  }

getParameters()方法,會再次處理參數類型,前面是包裝,這裏是拆封,因而,不管返回上面的哪個value,都是List<Student>或Collection集合,因而就可使用for(Object parameter : parameters)來迭代,迭代出來的parameter就是Student,Student的主鍵屬性爲keyProperty。

結論:使用SimpleExecutor和ReuseExecutor,執行foreach批量插入,能夠正確返回主鍵id列表。

然而,很惋惜,BatchExecutor卻存在bug,返回主鍵id列表爲null值。

4. BatchExecutor執行foreach批量插入,返回主鍵id列表爲null的緣由以及如何修復

每當提到批量插入,同窗們老是天然而然的想到BatchExecutor,這是程序員的本能。就像一想到交女友,就想到美女是同樣的道理。

BatchExecutor使用了一個BatchResult對象,來保存執行參數以及執行結果。

org.apache.ibatis.executor.BatchResult.java源碼。

public class BatchResult {

  private final List<Object> parameterObjects;
  // 竟然不建議使用了
  @Deprecated
  public Object getParameterObject() {
    return parameterObjects.get(0);
  }
  // 直接返回List<map>對象
  public List<Object> getParameterObjects() {
    return parameterObjects;
  }

  // 將parameterObject放到List中
  public void addParameterObject(Object parameterObject) {
    this.parameterObjects.add(parameterObject);
  }

前面已經講述了,List<Student>,被包裝爲Map<String, List<Student>>對象了,BatchResult又把Map<String, List<Student>>放到List中,因而,參數對象數據結構就變成了List<Map<String, List<Student>>>。

org.apache.ibatis.executor.BatchExecutor.doFlushStatements()方法源碼。

Jdbc3KeyGenerator jdbc3KeyGenerator = (Jdbc3KeyGenerator) keyGenerator;
jdbc3KeyGenerator.processBatch(ms, stmt, parameterObjects);

此時的parameterObjects對象,已是List<Map<String, List<Student>>>對象了,再執行for(Object parameter : parameterObjects)迭代,迭代出來的parameter是Map<String, List<Student>>對象,Map<String, List<Student>>對象固然沒有keyProperty指定的屬性了,指望迭代出來的目標對象是Student,而不是Map。因而,就產生了錯誤。因爲不能正確賦值,天然就沒法將主鍵id值,賦值給Student對象的主鍵屬性studId了,因此返回主鍵id值null,你們就認爲是Mybatis不支持,實際上是個誤會。

本身動手,修復該問題(修改BatchExecutor.doFlushStatements()方法源碼):

//Mybaits源碼
//jdbc3KeyGenerator.processBatch(ms, stmt, parameterObjects);

//修復後代碼
jdbc3KeyGenerator.processBatch(ms, stmt, this.getParameters(batchResult.getParameterObject()));

// org.apache.ibatis.executor.BatchExecutor中手動新增下面這個方法
public Collection<Object> getParameters(Object parameter) {
    Collection<Object> parameters = null;
    if (parameter instanceof Collection) {
      parameters = (Collection) parameter;
    } else if (parameter instanceof Map) {
      Map parameterMap = (Map) parameter;
      if (parameterMap.containsKey("collection")) {
        parameters = (Collection) parameterMap.get("collection");
      } else if (parameterMap.containsKey("list")) {
        parameters = (List) parameterMap.get("list");
      } else if (parameterMap.containsKey("array")) {
        parameters = Arrays.asList((Object[]) parameterMap.get("array"));
      }
    }
    if (parameters == null) {
      parameters = new ArrayList<Object>();
      parameters.add(parameter);
    }
    return parameters;
  }

解釋一下上面的代碼:

1. batchResult.getParameterObject()返回List<Map<String, List<Student>>>中的第0個元素(List長度自己就是1),因而獲得Map<String, List<Student>>對象。

2. getParameters(map)方法拆封,返回map的任一value對象,該value對象就是原始的List<Student>對象。該方法本是org.apache.ibatis.executor.keygen.Jdbc3KeyGenerator內的一個private方法,在外面不能調用,因而,複製一份出來,放到BatchExecutor中來使用。

3. for(Object parameter : parameters)迭代後,parameter就是Student元素,該元素有主鍵屬性studId,因而把數據庫返回的主鍵id值,賦給sutdId屬性。

通過以上三個步驟,咱們的BatchExecutor就能夠經過foreach批量插入,正確返回id列表了。

至此,SimpleExecutor、ReuseExecutor、BatchExecutor,都可以執行foreach批量插入,並正確返回id列表了。直接修改源代碼,有點暴力,後續講到plugin攔截器時,能夠再看看,有沒有更優雅的方式。

 

注:我不清楚Mybatis爲什麼要這麼設計,這究竟真是一個bug,仍是Mybatis故意爲之,只有時間能給出答案了。

 

版權提示:文章出自開源中國社區,若對文章感興趣,可關注個人開源中國社區博客(http://my.oschina.net/zudajun)。(通過網絡爬蟲或轉載的文章,常常丟失流程圖、時序圖,格式錯亂等,仍是看原版的比較好)

相關文章
相關標籤/搜索