背景

最近在做一个类似于综合报表之类的东西,需要查询所有的记录(数据库记录有限制),大概有1W条记录,该报表需要三个表的数据,也就是根据这 1W 个 ID 去执行查询三次数据库,其中,有一条查询 SQL 是自己写,其他两条是根据别人提供的接口进行查询,刚开始的时候,没有多想,直接使用 in 进行查询,使用 Mybatis 的 foreach 语句;项目中使用的是 jsonrpc 来请求数据,在测试的时候,发现老是请求不到数据,日志抛出的是 jsonrpc 超时异常,继续查看日志发现,是被阻塞在上面的三条SQL查询中。

在以前分析 Mybatis 的源码的时候,了解到,Mybatis 的 foreach 会有性能问题,所以改了下 SQL,直接在代码中拼接SQL,然后在 Mybatis 中直接使用 # 来获取,替换 class 测试了下,果然一下子就能查询出数据。

前提

这里先不考虑使用 in 好不好,如何去优化 in,如何使用 exists 或 inner join 进行代替等,这里就只是考虑使用了 in 语句,且使用了 Mybatis 的 foreach 语句进行优化,其实 foreach 的优化很简单,就是把 in 后面的语句在代码里面拼接好,在配置文件中直接通过 #{xxx} 或 ${xxx} 当作字符串直接使用即可。

测试

在分析 foreach 源码之前,先构造个数据来看看它们的区别有多大。

建表语句:

CREATE TABLE person(id int(11) PRIMARY KEY NOT NULL,name varchar(50),age int(11),job varchar(50));

插入 1W 条数据:

POJO 类:

@Getter@Setter@ToString@NoArgsConstructor@AllArgsConstructorpublic class Person implements Serializable {    private int id;    private String name;    private String job;    private int age;
}

方式一

通过原始的方式,使用 foreach 语句:

1. 在 dao 里面定义方法:

List<Person> queryPersonByIds(@Param("ids") List<Integer> ids);

2. 配置文件SQL:

<select id="queryPersonByIds" parameterType="list" resultMap="queryPersonMap">select * from person where 1=1 <if test="ids != null and ids.size() > 0">and id in        <foreach collection="ids" item="item" index="index" separator="," open="(" close=")">#{item}        </foreach></if></select>

3. 执行 main 方法:

@RunWith(SpringJUnit4Cla***unner.class)@ContextConfiguration(locations = { "classpath:spring-mybatis.xml" })public class MainTest {    @Autowiredprivate IPersonService personService;    @Testpublic void test(){        // 构造 1W 个 IDList<Integer> ids = new ArrayList<>();        for (int i = 1; i <= 10000; i++) {ids.add(i);}        long start = System.currentTimeMillis();        // 执行三次personService.queryPersonByIds(ids);personService.queryPersonByIds(ids);personService.queryPersonByIds(ids);        long end = System.currentTimeMillis();System.out.println(String.format("耗时:%d", end - start));}
}
结果:耗时:2853

可以看到通过 foreach 的方法,大概需要 3s

方式二

在代码中封装 SQL ,在配置文件中 通过 ${xxx} 来获取:

1. 在 dao 添加方法:

List<Person> queryPersonByIds2(@Param("ids") String ids);

2. 配置文件SQL:

<select id="queryPersonByIds2" parameterType="String" resultMap="queryPersonMap">select * from person where 1=1   <if test="ids != null and ids != ''">and id in ${ids}  </if></select>

3. 执行 main 方法:

@Testpublic void test_3(){    // 拼接 SQL StringBuffer sb = new StringBuffer();sb.append("(");   for (int i = 1; i < 10000; i++) {sb.append(i).append(",");}sb.deleteCharAt(sb.toString().length() - 1);sb.append(")");    // 最终的 SQL 为 (1,2,3,4,5...)long start2 = System.currentTimeMillis();    // 执行三次personService.queryPersonByIds2(sb.toString());personService.queryPersonByIds2(sb.toString());personService.queryPersonByIds2(sb.toString());   long end2 = System.currentTimeMillis();System.out.println(String.format("耗时:%d", end2 - start2));
}
结果:耗时:360

通过拼接 SQL,使用 ${xxx} 的方式,执行同样的 SQL ,耗时大概 360 ms

方式三

在代码中封装 SQL ,在配置文件中 通过 #{xxx} 来获取:

1. 在 dao 中添加方法:

List<Person> queryPersonByIds3(@Param("ids") String ids);

2. 配置文件SQL:

<select id="queryPersonByIds3" parameterType="String" resultMap="queryPersonMap">select * from person where 1=1  <if test="ids != null and ids != ''">and id in (#{ids})    </if></select>

3. 执行 main 方法:

@Testpublic void test_3(){    // 拼接 SQLStringBuffer sb2 = new StringBuffer();    for (int i = 1; i < 10000; i++) {sb2.append(i).append(",");}sb2.deleteCharAt(sb2.toString().length() - 1);    // 最终的SQL为 1,2,3,4,5....long start3 = System.currentTimeMillis();personService.queryPersonByIds3(sb2.toString());personService.queryPersonByIds3(sb2.toString());personService.queryPersonByIds3(sb2.toString());    long end3 = System.currentTimeMillis();System.out.println(String.format("耗时:%d", end3 - start3));
}
结果:耗时:30

通过拼接 SQL,使用 #{xxx} 的方式,执行同样的 SQL ,耗时大概 30 ms

总结

通过上面三种方式可以看到,使用不同的方式,耗时的差别还是麻大的,最快的是 拼接 SQL,使用 #{xxx} 当作字符串处理,最慢的是 foreach。为什么 foreach 会慢那么多呢,后面再分析源码的时候再进行分析;而这里同样是拼接 SQL 的方式,#{xxx} 和 ${xxx} 耗时却相差 10 倍左右;我们知道,Mybatis 在解析 # 和 $ 这两种不同的符号时,采用不同的处理策略;使用过 JDBC 的都知道,通过 JDBC 执行 SQL 有两种方式: Statment 对象和PreparedStatment 对象,  PreparedStatment 表示预编译的SQL,包含的SQL已经预编译过了,SQL 中的参数部分使用 ?进行占位,之后使用 setXXX 进行赋值,当使用 Statement 对象时,每次执行一个SQL命令时,都会对它进行解析和编译。所有 PreparedStatment 效率要高一些。那么 Mybatis 在解析 # 和 $ 的时候,分别对应的是这两种对象,# 被解析成 PreparedStatment 对象,通过 ? 进行占位,之后再赋值,而 $ 被解析成 Statement ,通过直接拼接SQL的方式赋值,所以,为什么同样是通过在代码中拼接 SQL ,# 和 $ 的耗时不同的原因。

PS:上面只是介绍了三种方式,应该没有人问,拼接SQL为 (1,2,3,4,5),在配置SQL中通过 #{xxx} 来获取吧

foreach 源码解析

下面来看下 foreach 是如何被解析的,最终解析的 SQL 是什么样的:

在 Mybatis 中,foreach 属于动态标签的一种,也是最智能的其中一种,Mybatis 每个动态标签都有对应的类来进行解析,而 foreach 主要是由 ForEachSqlNode 负责解析。

ForeachSqlNode 主要是用来解析 <foreach> 节点的,先来看看 <foreach> 节点的用法:

<select id="queryPersonByIds" parameterType="list" resultMap="queryPersonMap">select * from person where 1=1   <if test="ids != null and ids.size() > 0">and id in        <foreach collection="ids" item="item" index="index" separator="," open="(" close=")">#{item}        </foreach></if></select>

最终被 数据库执行的 SQL 为 select  * from person where 1=1 and id in (1,2,3,4,5)

先来看看它的两个内部类:

PrefixedContext

该类主要是用来处理前缀,比如 "(" 等。

private class PrefixedContext extends DynamicContext {   private DynamicContext delegate;    // 指定的前缀private String prefix;    // 是否处理过前缀private boolean prefixApplied;    // .......@Overridepublic void appendSql(String sql) {      // 如果还没有处理前缀,则添加前缀if (!prefixApplied && sql != null && sql.trim().length() > 0) {delegate.appendSql(prefix);prefixApplied = true;}       // 拼接SQLdelegate.appendSql(sql);}
}

FilteredDynamicContext

FilteredDynamicContext 是用来处理 #{} 占位符的,但是并未绑定参数,只是把 #{item} 转换为 #{_frch_item_1} 之类的占位符。

  private static class FilteredDynamicContext extends DynamicContext {    private DynamicContext delegate;    //对应集合项在集合的索引位置private int index;    // item的索引private String itemIndex;    // item的值private String item;    //.............// 解析 #{item}@Overridepublic void appendSql(String sql) {GenericTokenParser parser = new GenericTokenParser("#{", "}", new TokenHandler() {        @Overridepublic String handleToken(String content) {          // 把 #{itm} 转换为 #{__frch_item_1} 之类的String newContent = content.replaceFirst("^\\s*" + item + "(?![^.,:\\s])", itemizeItem(item, index));           // 把 #{itmIndex} 转换为 #{__frch_itemIndex_1} 之类的if (itemIndex != null && newContent.equals(content)) {newContent = content.replaceFirst("^\\s*" + itemIndex + "(?![^.,:\\s])", itemizeItem(itemIndex, index));}          // 再返回 #{__frch_item_1} 或 #{__frch_itemIndex_1}return new StringBuilder("#{").append(newContent).append("}").toString();}});      // 拼接SQLdelegate.appendSql(parser.parse(sql));}  private static String itemizeItem(String item, int i) {    return new StringBuilder("__frch_").append(item).append("_").append(i).toString();}
}

ForeachSqlNode

了解了 ForeachSqlNode  它的两个内部类之后,再来看看它的实现:

public class ForEachSqlNode implements SqlNode {  public static final String ITEM_PREFIX = "__frch_";  // 判断循环的终止条件private ExpressionEvaluator evaluator;  // 循环的集合private String collectionExpression;  // 子节点private SqlNode contents;  // 开始字符private String open;  // 结束字符private String close;  // 分隔符private String separator;  // 本次循环的元素,如果集合为 map,则index 为key,item为valueprivate String item;  // 本次循环的次数private String index;  private Configuration configuration;  // ...............@Overridepublic boolean apply(DynamicContext context) {    // 获取参数Map<String, Object> bindings = context.getBindings();    final Iterable<?> iterable = evaluator.evaluateIterable(collectionExpression, bindings);    if (!iterable.iterator().hasNext()) {      return true;}    boolean first = true;    // 添加开始字符串applyOpen(context);    int i = 0;    for (Object o : iterable) {DynamicContext oldContext = context;      if (first) {        // 如果是集合的第一项,则前缀prefix为空字符串context = new PrefixedContext(context, "");} else if (separator != null) {        // 如果分隔符不为空,则指定分隔符context = new PrefixedContext(context, separator);} else {          // 不指定分隔符,在默认为空context = new PrefixedContext(context, "");}      int uniqueNumber = context.getUniqueNumber();  if (o instanceof Map.Entry) {        // 如果集合是map类型,则将集合中的key和value添加到bindings参数集合中保存Map.Entry<Object, Object> mapEntry = (Map.Entry<Object, Object>) o;        // 所以循环的集合为map类型,则index为key,item为value,就是在这里设置的applyIndex(context, mapEntry.getKey(), uniqueNumber);applyItem(context, mapEntry.getValue(), uniqueNumber);} else {        // 不是map类型,则将集合中元素的索引和元素添加到 bindings集合中applyIndex(context, i, uniqueNumber);applyItem(context, o, uniqueNumber);}      // 调用 FilteredDynamicContext 的apply方法进行处理contents.apply(new FilteredDynamicContext(configuration, context, index, item, uniqueNumber));      if (first) {first = !((PrefixedContext) context).isPrefixApplied();}context = oldContext;i++;}     // 添加结束字符串applyClose(context);    return true;}  private void applyIndex(DynamicContext context, Object o, int i) {    if (index != null) {context.bind(index, o); // key为idnex,value为集合元素context.bind(itemizeItem(index, i), o); // 为index添加前缀和后缀形成新的key}}  private void applyItem(DynamicContext context, Object o, int i) {    if (item != null) {context.bind(item, o);context.bind(itemizeItem(item, i), o);}}
}

所以该例子:

<select id="queryPersonByIds" parameterType="list" resultMap="queryPersonMap">select * from person where 1=1    <if test="ids != null and ids.size() > 0">and id in        <foreach collection="ids" item="item" index="index" separator="," open="(" close=")">#{item}        </foreach></if></select>

解析之后的 SQL 为:

select  *  from  person where  1=1 and id in (#{__frch_item_0},  #{__frch_item_1}, #{__frch_item_2}, #{__frch_item_3}, #{__frch_item_4})

之后在通过 PreparedStatment 的 setXXX 来进行赋值。

所以,到这里,知道了 Mybatis 在解析 foreach 的时候,最后还是解析成了 # 的方式,但是为什么还是很慢呢,这是因为需要循环解析 #{__frch_item_0} 之类的占位符,foreach 的集合越大,解析越慢。既然知道了需要解析占位符,为何不自己拼接呢,所以就可以在代码中拼接好,而不再使用 foreach 啦。

所以,Mybatis 在解析 foreach 的时候,底层还是会解析成 # 号的形式而不是 的形式,既然知道了这个,如果 需要 foreach 的集合很大,就可以使用代码拼接 SQL ,使用 (#{xxx}) 的方式进行获取,不要再拼接成 (1,2,3,4,5) 再使用  ${xxx} 的方式啦。

转载于:https://blog.51cto.com/14227759/2364379

源码分析 Mybatis 的 foreach 为什么会出现性能问题相关推荐

  1. springboot集成mybatis源码分析-mybatis的mapper执行查询时的流程(三)

    springboot集成mybatis源码分析-mybatis的mapper执行查询时的流程(三) 例: package com.example.demo.service;import com.exa ...

  2. MyBatis源码分析——MyBatis的扩展点(pugins)

    1.MyBatis扩展点plugins mybatis的扩展是通过拦截器Interceptor来实现的,本质上就是JDK的动态代理,所以它只能对接口进行拦截,我们一步步看一下MyBatis是如何将这些 ...

  3. 源码分析 | Mybatis接口没有实现类为什么可以执行增删改查

    微信公众号:bugstack虫洞栈 | 案例源码:https://github.com/fuzhengwei/itstack-demo-code-mybatis 作为一款好用的ORM框架,一定是萝莉脸 ...

  4. MyBatis 源码分析-技术分享

    2019独角兽企业重金招聘Python工程师标准>>> MyBatis 源码分析 MyBatis的主要成员 Configuration MyBatis所有的配置信息都保存在Confi ...

  5. Mybatis源码分析: MapperMethod功能讲解

    canmengqian </div><!--end: blogTitle 博客的标题和副标题 --> <div id="navigator"> ...

  6. MyBatis源码分析(一)MyBatis整体架构分析

    文章目录 系列文章索引 一.为什么要用MyBatis 1.原始JDBC的痛点 2.Hibernate 和 JPA 3.MyBatis的特点 4.MyBatis整体架构 5.MyBatis主要组件及其相 ...

  7. MyBatis 源码分析 - 配置文件解析过程

    文章目录 * 本文速览 1.简介 2.配置文件解析过程分析 2.1 配置文件解析入口 2.2 解析 properties 配置 2.3 解析 settings 配置 2.3.1 settings 节点 ...

  8. statement执行insert into语句_【图文并茂】源码解析MyBatis ShardingJdbc SQL语句执行流程详解...

    源码分析Mybatis系列目录: 1.源码分析Mybatis MapperProxy初始化[图文并茂] 2.源码分析Mybatis MappedStatement的创建流程 3.[图文并茂]Mybat ...

  9. 源码分析Dubbo监控中心实现原理

       Dubbo监控的实现基本原理就是在服务调用时收集服务调用并发度.服务响应时间,然后以一定频率向监控中心汇报统计数据.    1.源码分析MonitorFilter过滤器 过滤器作用    监控过 ...

最新文章

  1. java通过异常处理错误,java基础之通过错误处理异常
  2. java串口发送16进制_串口发送数据——字符串发送与十六进制发送的区别
  3. 秀秀博客大赛50强的礼物
  4. [SHOI2015]自动刷题机
  5. java正则表示判断。是否以某个关键字结尾的
  6. 在featureDataset和workspace下創建featureclass
  7. 网络:窗口控制下的重发机制、流量控制
  8. JavaScript自定义浏览器滚动条兼容IE、 火狐和chrome
  9. Nginx多站点虚拟主机实现单独启动停止php-fpm、单独控制权限设置
  10. resteasy 统一异常_RESTEasy教程第3部分:异常处理
  11. c oracle实体模型,ADO.NET实体数据模型详细介绍
  12. A. Second Order Statistics(sort 水题)
  13. javascript window.open
  14. Google Maps Android API v2 (2)- 地图对象
  15. pip安装requirement.txt
  16. 【MYSQL快速入门】牛客网:多表查询
  17. Java数据类型分类 1
  18. MetalSeed 's 网站收藏夹
  19. 记第一次FPV模式飞行穿越机的感受
  20. 联想微型计算机改win7,[系统帮助]lenovo联想WIN8改WIN7详细图文全教程

热门文章

  1. android面试(4)---文件存储
  2. poj 2186(强连通分量)
  3. java性能、代码优化
  4. windows下利用批处理命令生成maven项目(java、javaWeb)
  5. LintCode2016年8月8日算法比赛----子树
  6. MongoDB的选举过程
  7. Oracle、 Mysql 、 SQLserver 分页查询
  8. php使用 memcache 来存储 session
  9. 【174个】最全DICOM资料汇总[截止10.10.20][中国数字医疗论坛]
  10. Jquery之AJAX用户注册表单验证(Vancl版)