mybatis中动态标签foreach解析过程分析

简要

     关于mybatis中的动态标签(常用的foreach、if、choose等),都会有对应的类去解析;SqlNode是*解析接口,各动态标签实现该接口的apply方法完成各自解析操作。foreach标签对应的解析实现类是foreachSqlNode.
     foreach标签解析的过程就是对foreach标签中的各个属性(collection、index、item等)进行解析处理的过程,每个属性都有对应的解析处理逻辑.sql解析完成之后执行对应的查询或删除等数据操作,然后封装结果集.本文仅对foreach标签解析进行说明.

执行demo

dao接口:

List<News> findNews(@Param("ids") List<Integer> ids);

xml配置文件:

 <select id="findNews" resultType="com.it.txm.demo.News">
        select id,title from find_news
         where id in
        (
        <foreach collection="ids" item="item" separator=",">
            #{item}
        </foreach>
        )
    </select>

测试demo:

ArrayList<Integer> list1 = new ArrayList<>();
	list1.add(777);
	list1.add(797);
	List<News> list = newsMapper.findNews(list1);
	System.out.println(list);

执行流程分析

debug调试进入获取BoundSql(即解析sql)
mybatis中动态标签foreach解析过程分析
    SqlNode为动态标签的*接口,有以下实现类.
mybatis中动态标签foreach解析过程分析

    MixedSqlNode可以认为是多个动态标签的组合处理者,会循环处理每个标签.源码:

public class MixedSqlNode implements SqlNode {
  private final List<SqlNode> contents;
	// 创建MixedSqlNode对象时会将实现sqlNode实现类集合添加到MixedSqlNode中
  public MixedSqlNode(List<SqlNode> contents) {
    this.contents = contents;
  }
	// MixedSqlNode对apply方法的实现处理就是执行对各个标签的apply实现逻辑.
  @Override
  public boolean apply(DynamicContext context) {
    contents.forEach(node -> node.apply(context));
    return true;
  }
}

    具体到demo中,mixedSqlNode有两个sqlNode接口的实现类:
StaticTextSqlNodeForEachSqlNode;
StaticTextSqlNode实现apply做的处理是对sql的拼接,源码如下:

public class StaticTextSqlNode implements SqlNode {
  private final String text;

  public StaticTextSqlNode(String text) {
    this.text = text;
  }
	// 动态上下文对象进行拼接sql
  @Override
  public boolean apply(DynamicContext context) {
    context.appendSql(text);
    return true;
  }
  // 最终是利用sqlBuilder进行参数拼接
	public void appendSql(String sql) {
	    sqlBuilder.add(sql);
	  }
}

    具体到案例中StaticTextSqlNode实际解析的就是下面标注的两行sql:
mybatis中动态标签foreach解析过程分析
    以下说明与源码中实际解析执行的一致:
mybatis中动态标签foreach解析过程分析

    下面主要说ForEachSqlNode,源码如下:

public boolean apply(DynamicContext context) {
	// 从动态上下文中获取映射绑定信息(这里只用到参数映射信息.)
    Map<String, Object> bindings = context.getBindings();
    // 解析foreach标签中的collection属性.返回迭代器类型参数,实际就是解析的collection标签中集合的具体参数值:777 797
    final Iterable<?> iterable = evaluator.evaluateIterable(collectionExpression, bindings);
    if (!iterable.iterator().hasNext()) {
      return true;
    }
    boolean first = true;
    // 解析foreach标签中的open属性.具体解析处理方式就是sql拼接,由于比较简单,这里不再展开.
    applyOpen(context);
    int i = 0;
    // 遍历迭代器中的实参.
    for (Object o : iterable) {
      DynamicContext oldContext = context{
      // 此处处理foreach标签中的separator分隔符属性,new PrefixedContext()实际上执行的是DynamicContext中设置分割属性.如果 不为空则设置为foreach标签中实际使用的分割符为空则设置空字符串,执行demo中使用的是逗号.
      if (first || separator == null) {
        context = new PrefixedContext(context, "");
      } else {
        context = new PrefixedContext(context, separator);
      }
      int uniqueNumber = context.getUniqueNumber();
      // Issue #709
      if (o instanceof Map.Entry) {
        @SuppressWarnings("unchecked")
        Map.Entry<Object, Object> mapEntry = (Map.Entry<Object, Object>) o;
        applyIndex(context, mapEntry.getKey(), uniqueNumber);
        applyItem(context, mapEntry.getValue(), uniqueNumber);
      } else {
		// applyIndex处理的是foreach标签中index属性,下面会展开说
        applyIndex(context, i, uniqueNumber);
        // applyItem主要作用是解析foreach标签中的item属性并进行赋值,下面会展开说明
        applyItem(context, o, uniqueNumber);
      }
      // 将foreach标签中解析完成的单个参数进行拼接
      contents.apply(new FilteredDynamicContext(configuration, context, index, item, uniqueNumber));
      if (first) {
        first = !((PrefixedContext) context).isPrefixApplied();
      }
      context = oldContext;
      i++;
    }
    // 执行foreach标签中close属性对应处理,实际是执行参数拼接.
    applyClose(context);
    context.getBindings().remove(item);
    context.getBindings().remove(index);
    return true;
  }

    简单说一下DynamicContext,可以理解为是动态参数的上下文对象,里面map形式的方法参数、用于sql拼接的sqlBuilderd.
mybatis中动态标签foreach解析过程分析
    applyIndex主要作用是处理foreach标签中的index属性,按照map形式往DynamicContext动态上下文中存入对象信息,最终封装数:context.bind中执行bindings.put(name, value);

  private void applyIndex(DynamicContext context, Object o, int i) {
  // 对于foreach标签中index属性设置的情况下执行以下逻辑.
    if (index != null) {
    	// 按照index为key,实际参数o的形式组装map存入DynamicContext动态sql上下文中.
      context.bind(index, o);
      // itemizeItem作用参数拼装,foreach中解析之后的格式默认:__frch_ + item + "_" + i,然后按照key-value形式存入DynamicContext动态sql上下文中.
      context.bind(itemizeItem(index, i), o);
    }
  }

    applyItem执行原理同applyIndex,foreach标签中item属性不为空时执行:按照key-value形式往DynamicContext动态上下文中存入对象信息.

  private void applyItem(DynamicContext context, Object o, int i) {
    if (item != null) {
      context.bind(item, o);
      context.bind(itemizeItem(item, i), o);
    }
  }

    执行完成之后DynamicContext中的sql拼接对象中组装的查询sql如下:
mybatis中动态标签foreach解析过程分析
至此foreach标签解析分析完成.
    欢迎小伙伴评论区留言,共同交流共同进步!

上一篇:Mybatis 多表操作


下一篇:Lambda