MyBatis中<foreach> 执行效率

文章目录

    • 概要
    • 整体架构流程
    • 技术名词解释
    • 技术细节
    • 小结

概要

MyBatis中<foreach> 执行效率。

整体架构流程

MyBatis

技术名词解释

MyBatis

技术细节

这个方法是将传统的insert

转换为:

看上去没什么问题,但是经过项目实践当list数量超过(5000+)时,整个插入的耗时十分漫长,达到了14分钟,这是不能忍的。在资料中也提到了一句话:

Of course don't combine ALL of them, if the amount is HUGE. Say you have 1000 rows you need to insert, then don't do it one at a time. You shouldn't equally try to have all 1000 rows in a single query. Instead break it into smaller sizes.

当然,如果数量巨大,不要将所有这些都结合起来。假设您需要插入1000行,那么不要一次插入一行。您不应该尝试在单个查询中包含所有1000行。而是把它分成更小的尺寸

为什么会耗时这么长呢?查阅了资料发现:

Insert inside Mybatis foreach is not batch, this is a single (could become giant) SQL statement and that brings drawbacks:

在mysql的foreach中插入不是批处理的,这是一个单一的(可能成为巨大的)SQL语句,这带来了缺点:

  • some database such as Oracle here does not support.

  • in relevant cases: there will be a large number of records to insert and the database configured limit (by default around 2000 parameters per statement) will be hit, and eventually possibly DB stack error if the statement itself become too large.

  • 一些数据库如Oracle不支持。

  • 在相关的情况下:将会有大量的记录要插入,并且会达到数据库配置的限制(默认情况下每条语句大约2000个参数),如果语句本身变得太大,最终可能会出现DB堆栈错误。

Iteration over the collection must not be done in the mybatis XML. Just execute a simple Insertstatement in a Java Foreach loop. The most important thing is the session Executor type.

不能在mybatis XML中对集合进行迭代。只需在Java Foreach循环中执行一个简单的Insertstatement。最重要的是会话执行者类型。

SqlSession session = sessionFactory.openSession(ExecutorType.BATCH);
for (Model model : list) {
    session.insert("insertStatement", model);
}
session.flushStatements();

从资料中得知,默认执行器类型为Simple,会为每个语句创建一个新的预处理语句,也就是创建一个PreparedStatement对象。在我们的项目中,会不停地使用批量插入这个方法,而因为MyBatis对于含有<foreach>的语句,无法采用缓存,那么在每次调用方法时,都会重新解析sql语句。

Internally, it still generates the same single insert statement with many placeholders as the JDBC code above.

MyBatis has an ability to cache PreparedStatement, but this statement cannot be cached because it contains <foreach /> element and the statement varies depending on the parameters. As a result, MyBatis has to 1) evaluate the foreach part and 2) parse the statement string to build parameter mapping [1] on every execution of this statement.

And these steps are relatively costly process when the statement string is big and contains many placeholders.

[1] simply put, it is a mapping between placeholders and the parameters.

在内部,它仍然生成与上面JDBC代码相同的单个插入语句,其中包含许多占位符。

MyBatis有缓存PreparedStatement的能力,但是这个语句不能被缓存,因为它包含<foreach />元素,并且语句根据参数而变化。因此,MyBatis必须1)计算foreach部分,2)解析语句字符串,以便在每次执行该语句时构建参数映射[1]。

当语句字符串很大并且包含许多占位符时,这些步骤的成本相对较高。

[1]简单地说,它是占位符和参数之间的映射。

从上述资料中得知,耗时就耗在,由于我foreach后有5000+个values,所以这个PreparedStatement特别长,包含了很多占位符,对于占位符和参数的映射尤其耗时。并且,查阅相关资料可知,values的增长与所需的解析时间,是呈指数型增长的。

重点来了。上面讲的是,如果非要用<foreach>的方式来插入,可以提升性能的方式。而实际上,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的下面语句一样。

小结

总结一下,如果MyBatis需要进行批量插入,推荐使用 ExecutorType.BATCH 的插入方式,如果非要使用 <foreach>的插入的话,需要将每次插入的记录控制在 20~50 左右。