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)。(通过网络爬虫或转载的文章,常常丢失流程图、时序图,格式错乱等,仍是看原版的比较好)