mybatis的坑-foreach

发布时间 2023-04-13 10:54:32作者: 星期一天气晴

往MyBatis中批量插入数据,我们常常这么干

<insert id="batchInsert" parameterType="java.util.List">  
    insert into USER (id, name) values  
    <foreach collection="list" item="model" index="index" separator=",">   
        (#{model.id}, #{model.name})  
    </foreach>  
</insert>

这个方法提升批量插入速度的原理是,将传统的:

INSERT INTO `user` (`name`, `age`) VALUES ("zs", "10");  
INSERT INTO `user` (`name`, `age`) VALUES ("ls", "12");  
INSERT INTO `user` (`name`, `age`) VALUES ("ww", "14");  
INSERT INTO `user` (`name`, `age`) VALUES ("hl", "11");  
INSERT INTO `user` (`name`, `age`) VALUES ("sw", "54");

转化为:

INSERT INTO `user` (`name`, `age`)   
      VALUES ("zs", "10"),  
             ("ls", "12"),  
             ("ww", "14"),  
             ("hl", "11"),  
             ("sw", "54"); 

  理想情况下,如果要优化插入速度时,可以将许多小型操作组合到一个大型操作中,这样可以在单个连接中一次性发送许多新行的数据,并将所有索引更新和一致性检查延迟到最后才进行。

  乍看上去这个foreach没有问题,但是经过项目实践发现,当表的列数较多(20+),以及一次性插入的行数较多(5000+)时,整个插入的耗时十分漫长,这是不能忍的。

  当插入数量很多时,不能一次性全放在一条语句里。因为默认执行器类型为Simple,会为每个语句创建一个新的预处理语句,也就是创建一个PreparedStatement对象。当我们不停地使用这个批量插入方法,而MyBatis对于含有<foreach>的语句,无法采用缓存,那么在每次调用方法时,都会重新解析sql语句。

  如果我们的foreach后有5000+个values,那么这个PreparedStatement特别长,他包含了很多占位符,对于占位符和参数的映射尤其耗时。查阅相关资料可知,values的增长与所需的解析时间,是呈指数型增长的。

  所以,如果非要使用 foreach 的方式来进行批量插入的话,可以考虑减少一条 insert 语句中 values 的个数,最好能达到上面曲线的最底部的值,使速度最快。一般按经验来说,一次性插20~50行数量是比较合适的,时间消耗也能接受。

  而提升性能的方式,MyBatis文档中推荐使用另外一种方法写批量插入。(可以看 http://www.mybatis.org/mybatis-dynamic-sql/docs/insert.html 中 Batch Insert Support 标题里的内容)

SqlSession session = sqlSessionFactory.openSession(ExecutorType.BATCH);  
try {  
    SimpleTableMapper mapper = session.getMapper(SimpleTableMapper.class);  
    List<SimpleTableRecord> records = getRecordsToInsert(); // not shown  
   
    BatchInsert<SimpleTableRecord> batchInsert = insert(records)  
            .into(simpleTable)  
            .map(id).toProperty("id")  
            .map(firstName).toProperty("firstName")  
            .map(lastName).toProperty("lastName")  
            .map(birthDate).toProperty("birthDate")  
            .map(employed).toProperty("employed")  
            .map(occupation).toProperty("occupation")  
            .build()  
            .render(RenderingStrategy.MYBATIS3);  
   
    batchInsert.insertStatements().stream().forEach(mapper::insert);  
   
    session.commit();  
} finally {  
    session.close();  
}  

  基本思想是将 MyBatis session 的 executor type 设为 Batch ,然后多次执行插入语句。就类似于JDBC的下面语句一样。

Connection connection = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/mydb?useUnicode=true&characterEncoding=UTF-8&useServerPrepStmts=false&rewriteBatchedStatements=true","root","root");  
connection.setAutoCommit(false);  
PreparedStatement ps = connection.prepareStatement(  
        "insert into tb_user (name) values(?)");  
for (int i = 0; i < stuNum; i++) {  
    ps.setString(1,name);  
    ps.addBatch();  
}  
ps.executeBatch();  
connection.commit();  
connection.close(); 

  使用 ExecutorType.BATCH 的插入方式后,性能会显著提升。如果MyBatis需要进行批量插入,推荐使用 ExecutorType.BATCH 的插入方式,如果非要使用 <foreach> 的插入的话,需要将每次插入的记录控制在 20~50 左右。