mybatis源码学习------动态sql的解析(SqlNode)

介绍

SqlNode是mybatis动态SQL功能的重要组成部分,关于动态sql,官网对其的介绍如下:

动态 SQL 是 MyBatis 的强大特性之一。如果你使用过 JDBC 或其它类似的框架,你应该能理解根据不同条件拼接 SQL 语句有多痛苦,例如拼接时要确保不能忘记添加必要的空格,还要注意去掉列表最后一个列名的逗号。利用动态 SQL,可以彻底摆脱这种痛苦。

使用动态 SQL 并非一件易事,但借助可用于任何 SQL 映射语句中的强大的动态 SQL 语言,MyBatis 显著地提升了这一特性的易用性。

如果你之前用过 JSTL 或任何基于类 XML 语言的文本处理器,你对动态 SQL 元素可能会感觉似曾相识。在 MyBatis 之前的版本中,需要花时间了解大量的元素。借助功能强大的基于 OGNL 的表达式,MyBatis 3 替换了之前的大部分元素,大大精简了元素种类,现在要学习的元素种类比原来的一半还要少。

  • if
  • choose (when, otherwise)
  • trim (where, set)
  • foreach

例如下面这段sql中,<if></if>标签所包含的内容则是mybatis动态sql功能最基本的体现,他能实现如果不传入 “title”,那么所有处于 “ACTIVE” 状态的 BLOG 都会返回;如果传入了 “title” 参数,那么就会对 “title” 一列进行模糊查找并返回对应的 BLOG 结果

<select id="findActiveBlogWithTitleLike"
     resultType="Blog">
  SELECT * FROM BLOG
  WHERE state = ‘ACTIVE’
  <if test="title != null">
    AND title like #{title}
  </if>
</select>

mybatis提供的动态sql类型有:

  • if
  • choose
  • when
  • otherwise
  • trim
  • where
  • set
  • foreach
  • script
  • bind

本文章的目的在于从源码实现的角度分析mybatis框架

  1. 是如何解析用户关于动态sql的配置
  2. 动态sql在内存中的数据结构是什么

SqlSource和SqlNode的关系

文章开始之前需要搞明白代码中的SqlSource和SqlNode分别是什么,以及他们的关系是什么。这两个关系搞不清的话,看代码就会比较懵逼。
在这里插入图片描述
他们的关系如上图所示,SqlSource和SqlNode的关系是一对多的关系。

SqlNode

如上所述,SqlNode是动态sql配置在程序中的组织形式,每个 XML Node 会解析成对应的 SqlNode 对象。其类图如下:

在这里插入图片描述

SqlNode的各个实现类使用了组合设计模式,mybatis通过使用组合设计模式帮助上层调用者屏蔽了对象的复杂性,也使得如果后续需要添加新的SqlNode类型的话,会变得非常容易,符合开闭原则。

关于组合设计模式,可以参考这篇文章https://www.runoob.com/design-pattern/composite-pattern.html

SqlNode接口

public interface SqlNode {
  /**
   *
   * @param context 上下文,在执行时该对象会持有用户传入的实际参数
   * @return 当前 SQL Node 节点是否应用成功
   */
  boolean apply(DynamicContext context);
}

IfSqlNode

对应于下面例子中的<if></if>节点

<select id="findActiveBlogWithTitleLike"
     resultType="Blog">
  SELECT * FROM BLOG
  WHERE state = ‘ACTIVE’
  <if test="title != null">
    AND title like #{title}
  </if>
</select>

IfSqlNode类的定义

public class IfSqlNode implements SqlNode {
  //表达式解析器,用于判断 "if test='条件'"是否成立
  private final ExpressionEvaluator evaluator;
  //成立条件
  private final String test;
  //if节点的子节点
  private final SqlNode contents;

  public IfSqlNode(SqlNode contents, String test) {
    this.test = test;
    this.contents = contents;
    this.evaluator = new ExpressionEvaluator();
  }

  /**
   * 如果当前if节点的条件成立,则判断子节点的条件是否成立
   * @param context 上下文,在执行时该对象会持有用户传入的实际参数
   * @return
   */
  @Override
  public boolean apply(DynamicContext context) {
    if (evaluator.evaluateBoolean(test, context.getBindings())) {
      //子节点是否成立不能影响当前节点的判断结果,所以这里没有直接返回
      contents.apply(context);
      return true;
    }
    return false;
  }
}

StaticTextSqlNode

StaticTextSqlNode表示的是文本类型,其中的文本不包含任何动态元素,即不包括${}这种占位符。对应于下图红框中的内容

在这里插入图片描述
StaticTextSqlNode类的定义:

public class StaticTextSqlNode implements SqlNode {
  private final String text;

  public StaticTextSqlNode(String text) {
    this.text = text;
  }

//因为StaticTextSqlNode表示不包含任何动态元素的节点,所以不需要对其做任何处理,
//直接将sql追加上下文对象中即可
  @Override
  public boolean apply(DynamicContext context) {
    context.appendSql(text);
    return true;
  }
}

TextSqlNode

TextSqlNode表示的是包含${}占位符的动态字符串节点,对应于下图中的红框部分:

在这里插入图片描述
TextSqlNode类的定义:

public class TextSqlNode implements SqlNode {
  //包含占位符的原始文本字符串
  private final String text;
  //校验实参的正则对象
  private final Pattern injectionFilter;

  public TextSqlNode(String text) {
    //调用重载的构造函数
    this(text, null);
  }

  public TextSqlNode(String text, Pattern injectionFilter) {
    this.text = text;
    this.injectionFilter = injectionFilter;
  }

  /**
   * 判断当前文本字符串是否包含 ${} 占位符,各种判断字符串中是否存在占位符 ${}
   * @return
   */
  public boolean isDynamic() {
    DynamicCheckerTokenParser checker = new DynamicCheckerTokenParser();
    GenericTokenParser parser = createParser(checker);
    parser.parse(text);
    return checker.isDynamic();
  }

  /**
   * 从上下文中获取用户传入的实际参数,并使用实际参数替换对应的占位符
   * 再通过 injectionFilter 字段所表示的正则对实参进行校验,最后将处理后的sql字符串添加到上下文中。
   * @param context 上下文,在执行时该对象会持有用户传入的实际参数
   * @return
   */
  @Override
  public boolean apply(DynamicContext context) {
    GenericTokenParser parser = createParser(new BindingTokenParser(context, injectionFilter));
    context.appendSql(parser.parse(text));
    return true;
  }

  private GenericTokenParser createParser(TokenHandler handler) {
    return new GenericTokenParser("${", "}", handler);
  }

  private static class BindingTokenParser implements TokenHandler {

    private DynamicContext context;
    private Pattern injectionFilter;

    public BindingTokenParser(DynamicContext context, Pattern injectionFilter) {
      this.context = context;
      this.injectionFilter = injectionFilter;
    }

    @Override
    public String handleToken(String content) {
      Object parameter = context.getBindings().get("_parameter");
      if (parameter == null) {
        context.getBindings().put("value", null);
      } else if (SimpleTypeRegistry.isSimpleType(parameter.getClass())) {//是否为简单类型
        context.getBindings().put("value", parameter);
      }
      //从上下文中获取实际参数
      Object value = OgnlCache.getValue(content, context.getBindings());
      String srtValue = value == null ? "" : String.valueOf(value); // issue #274 return "" instead of "null"
      //通过正则匹配的方式检查实参是否合法
      checkInjection(srtValue);
      return srtValue;
    }

    private void checkInjection(String value) {
      if (injectionFilter != null && !injectionFilter.matcher(value).matches()) {
        throw new ScriptingException("Invalid input. Please conform to regex" + injectionFilter.pattern());
      }
    }
  }

  private static class DynamicCheckerTokenParser implements TokenHandler {

    private boolean isDynamic;

    public DynamicCheckerTokenParser() {
      // Prevent Synthetic Access
    }

    public boolean isDynamic() {
      return isDynamic;
    }

    @Override
    public String handleToken(String content) {
      this.isDynamic = true;
      return null;
    }
  }
}

ChooseSqlNode

choose元素和java中的switch语句相似,对应于下图红框中的内容:

在这里插入图片描述

ChooseSqlNode类的定义:

/**
 * 由<choose></choose>标签的DTD定义可知,<when></when>标签可以有多个,而<otherwise></otherwise>只能有
 * 一个。所以defaultSqlNode为一个对象,而ifSqlNodes是一个集合。
 */
public class ChooseSqlNode implements SqlNode {
  //对应于<otherwise></otherwise>
  private final SqlNode defaultSqlNode;
  //对应于<when></when>
  private final List<SqlNode> ifSqlNodes;

  public ChooseSqlNode(List<SqlNode> ifSqlNodes, SqlNode defaultSqlNode) {
    this.ifSqlNodes = ifSqlNodes;
    this.defaultSqlNode = defaultSqlNode;
  }

  @Override
  public boolean apply(DynamicContext context) {
    //遍历ifSqlNodes集合,查找条件成立的SqlNode
    for (SqlNode sqlNode : ifSqlNodes) {
      if (sqlNode.apply(context)) {
        return true;
      }
    }
    //如果ifSqlNodes集合中的所有节点的条件都不成立,则使用该兜底方案
    if (defaultSqlNode != null) {
      defaultSqlNode.apply(context);
      return true;
    }
    return false;
  }
}

VarDeclSqlNode

对应于<bind></bind>标签,bind 元素允许你在 OGNL 表达式以外创建一个变量,并将其绑定到当前的上下文。比如:

<select id="selectBlogsLike" resultType="Blog">
  <bind name="pattern" value="'%' + _parameter.getTitle() + '%'" />
  SELECT * FROM BLOG
  WHERE title LIKE #{pattern}
</select>

VarDeclSqlNode类的定义:

public class VarDeclSqlNode implements SqlNode {
  //对应<bind/>的name属性
  private final String name;
  //对应<bind/>的value属性
  private final String expression;

  public VarDeclSqlNode(String var, String exp) {
    name = var;
    expression = exp;
  }

  @Override
  public boolean apply(DynamicContext context) {
    //从Ognl缓存中获取值
    final Object value = OgnlCache.getValue(expression, context.getBindings());
    context.bind(name, value);
    return true;
  }
}

MixedSqlNode

MixedSqlNode可以理解为组合模式中的树枝,其他类型的SqlNode都是树叶,从其类的定义中可以看到,他只是持有了很多个其他SqlNode对象而已。

MixedSqlNode类的定义:

public class MixedSqlNode implements SqlNode {
  private final List<SqlNode> contents;

  public MixedSqlNode(List<SqlNode> contents) {
    this.contents = contents;
  }

  @Override
  public boolean apply(DynamicContext context) {
    contents.forEach(node -> node.apply(context));
    return true;
  }
}

ForEachSqlNode

<foreach/>的介绍如下:

foreach 元素的功能非常强大,它允许你指定一个集合,声明可以在元素体内使用的集合项(item)和索引(index)变量。它也允许你指定开头与结尾的字符串以及集合项迭代之间的分隔符。这个元素也不会错误地添加多余的分隔符,看它多智能!

提示 你可以将任何可迭代对象(如 List、Set 等)、Map 对象或者数组对象作为集合参数传递给 foreach。当使用可迭代对象或者数组时,index 是当前迭代的序号,item 的值是本次迭代获取到的元素。当使用 Map 对象(或者 Map.Entry 对象的集合)时,index 是键,item 是值。

对应于下图中的红框部分:
在这里插入图片描述

ForEachSqlNode类的定义:

public class ForEachSqlNode implements SqlNode {
  public static final String ITEM_PREFIX = "__frch_";
  //表达式解析器
  private final ExpressionEvaluator evaluator;
  //<foreach/>的collection属性
  private final String collectionExpression;
  //<foreach/>标签的内容所封装的SqlNode对象
  private final SqlNode contents;
  //<foreach/>的open属性
  private final String open;
  //<foreach/>的close属性
  private final String close;
  //<foreach/>的separator属性
  private final String separator;
  //<foreach/>的item属性
  private final String item;
  //<foreach/>的index属性
  private final String index;
  //全局配置对象
  private final Configuration configuration;

  public ForEachSqlNode(Configuration configuration, SqlNode contents, String collectionExpression, String index, String item, String open, String close, String separator) {
    this.evaluator = new ExpressionEvaluator();
    this.collectionExpression = collectionExpression;
    this.contents = contents;
    this.open = open;
    this.close = close;
    this.separator = separator;
    this.index = index;
    this.item = item;
    this.configuration = configuration;
  }

  @Override
  public boolean apply(DynamicContext context) {
    Map<String, Object> bindings = context.getBindings();
    //获得表达式对应的集合,无论expression对应的实际类型是数组还是map,最后都会给处理成集合的形式
    final Iterable<?> iterable = evaluator.evaluateIterable(collectionExpression, bindings);
    //如果为空集合,则直接返回true
    if (!iterable.iterator().hasNext()) {
      return true;
    }
    //是否为集合的第一个元素的标志位  
    boolean first = true;
    //拼接"("  
    applyOpen(context);
    //遍历的索引
    int i = 0;
    for (Object o : iterable) {
      DynamicContext oldContext = context;
      if (first || separator == null) {
        //如果是第一个元素,则不需要对其添加分隔符
        context = new PrefixedContext(context, "");
      } else {
        //其余元素都需要添加分隔符
        context = new PrefixedContext(context, separator);
      }
      //因为集合中的每一个元素都会有一个唯一对应的PrefixedContext对象,所以这个number可以认为在当前集合
      //中是唯一的
      int uniqueNumber = context.getUniqueNumber();
      //转换在上下文中的占位符
      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(context, i, uniqueNumber);
        applyItem(context, o, uniqueNumber);
      }
      //处理子SqlNode节点  
      contents.apply(new FilteredDynamicContext(configuration, context, index, item, uniqueNumber));
      if (first) {
        first = !((PrefixedContext) context).isPrefixApplied();
      }
      context = oldContext;
      i++;
    }
    //追加")"
    applyClose(context);
    context.getBindings().remove(item);
    context.getBindings().remove(index);
    return true;
  }
  //处理索引与实参之间的关系
  private void applyIndex(DynamicContext context, Object o, int i) {
    //如果遍历的是Map,则实参为集合的key
    if (index != null) {
      //绑定索引和实际对象的关系
      context.bind(index, o);
      //__frch_+index+i ---> 实际对象
      context.bind(itemizeItem(index, i), o);
    }
  }
  //处理item与实参的关系
  private void applyItem(DynamicContext context, Object o, int i) {
    //如果遍历的是Map,则实参为集合的value
    if (item != null) {
      // item ---> 实际对象
      context.bind(item, o);
      // __frch_+item+i ---> 实际对象
      context.bind(itemizeItem(item, i), o);
    }
  }
  //直接追加open,也就是 "("
  private void applyOpen(DynamicContext context) {
    if (open != null) {
      context.appendSql(open);
    }
  }
  //直接追加close,也就是 "("
  private void applyClose(DynamicContext context) {
    if (close != null) {
      context.appendSql(close);
    }
  }
  //拼接成 __frch_+index+i 的格式
  private static String itemizeItem(String item, int i) {
    return ITEM_PREFIX + item + "_" + i;
  }

  /**
   * 继承自DynamicContext类,重写了父类的appendSql方法
   * 是父类DynamicContext的静态代理类
   */
  private static class FilteredDynamicContext extends DynamicContext {
    private final DynamicContext delegate;
    private final int index;
    private final String itemIndex;
    private final String item;

    public FilteredDynamicContext(Configuration configuration,DynamicContext delegate, String itemIndex, String item, int i) {
      super(configuration, null);
      this.delegate = delegate;
      this.index = i;
      this.itemIndex = itemIndex;
      this.item = item;
    }

    @Override
    public Map<String, Object> getBindings() {
      return delegate.getBindings();
    }

    @Override
    public void bind(String name, Object value) {
      delegate.bind(name, value);
    }

    @Override
    public String getSql() {
      return delegate.getSql();
    }

    /**
     * 将#{item} 拼接成 #{__frch_+item+i} 的形式
     * @param sql
     */
    @Override
    public void appendSql(String sql) {
      GenericTokenParser parser = new GenericTokenParser("#{", "}", content -> {
        String newContent = content.replaceFirst("^\\s*" + item + "(?![^.,:\\s])", itemizeItem(item, index));
        if (itemIndex != null && newContent.equals(content)) {
          newContent = content.replaceFirst("^\\s*" + itemIndex + "(?![^.,:\\s])", itemizeItem(itemIndex, index));
        }
        return "#{" + newContent + "}";
      });

      delegate.appendSql(parser.parse(sql));
    }

    @Override
    public int getUniqueNumber() {
      return delegate.getUniqueNumber();
    }

  }

  /**
   * 继承自DynamicContext类,重写了父类的appendSql方法
   * 是父类DynamicContext的静态代理类
   */
  private class PrefixedContext extends DynamicContext {
    private final DynamicContext delegate;
    private final String prefix;
    //表示前缀是否已经处理过
    private boolean prefixApplied;

    public PrefixedContext(DynamicContext delegate, String prefix) {
      super(configuration, null);
      this.delegate = delegate;
      this.prefix = prefix;
      this.prefixApplied = false;
    }

    public boolean isPrefixApplied() {
      return prefixApplied;
    }

    @Override
    public Map<String, Object> getBindings() {
      return delegate.getBindings();
    }

    @Override
    public void bind(String name, Object value) {
      delegate.bind(name, value);
    }

    /**
     * 在追加sql之前先拼接一个前缀
     * @param sql 待处理的sql
     */
    @Override
    public void appendSql(String sql) {
      if (!prefixApplied && sql != null && sql.trim().length() > 0) {
        //先添加前缀
        delegate.appendSql(prefix);
        prefixApplied = true;
      }
      delegate.appendSql(sql);
    }

    @Override
    public String getSql() {
      return delegate.getSql();
    }

    @Override
    public int getUniqueNumber() {
      return delegate.getUniqueNumber();
    }
  }
}

TrimSqlNode

对于下面的例子:

<select id="findActiveBlogLike"
     resultType="Blog">
  SELECT * FROM BLOG
  WHERE
  <if test="state != null">
    state = #{state}
  </if>
  <if test="title != null">
    AND title like #{title}
  </if>
  <if test="author != null and author.name != null">
    AND author_name like #{author.name}
  </if>
</select>

如果没有匹配的条件会怎么样?最终这条 SQL 会变成这样:

SELECT * FROM BLOG
WHERE

这会导致查询失败。如果匹配的只是第二个条件又会怎样?这条 SQL 会是这样:

SELECT * FROM BLOG
WHERE
AND title like ‘someTitle’

这个查询也会失败。这个问题不能简单地用条件元素来解决。这个问题是如此的难以解决,以至于解决过的人不会再想碰到这种问题。

MyBatis 提供了一个简单且适合大多数场景的解决办法。而在其他场景中,可以对其进行自定义以符合需求。而这,只需要一处简单的改动:

<select id="findActiveBlogLike"
     resultType="Blog">
  SELECT * FROM BLOG
  <where>
    <if test="state != null">
         state = #{state}
    </if>
    <if test="title != null">
        AND title like #{title}
    </if>
    <if test="author != null and author.name != null">
        AND author_name like #{author.name}
    </if>
  </where>
</select>

如果 where 元素与你期望的不太一样,你也可以通过自定义 trim 元素来定制 where 元素的功能。比如,和 where 元素等价的自定义 trim 元素为:

<trim prefix="WHERE" prefixOverrides="AND |OR ">
  ...
</trim>

prefixOverrides 属性会忽略通过管道符分隔的文本序列(注意此例中的空格是必要的)。上述例子会移除所有 prefixOverrides 属性中指定的内容,并且插入 prefix 属性中指定的内容。

用于动态更新语句的类似解决方案叫做 setset 元素可以用于动态包含需要更新的列,忽略其它不更新的列。

TrimSqlNode类的定义:

public class TrimSqlNode implements SqlNode {
  //<trim></trim>标签内部定义的SqlNode节点
  private final SqlNode contents;
  //前缀
  private final String prefix;
  //后缀
  private final String suffix;
  //需要被忽略的前缀值
  private final List<String> prefixesToOverride;
  //需要被忽略的后缀值
  private final List<String> suffixesToOverride;
  //全局配置对象
  private final Configuration configuration;

  public TrimSqlNode(Configuration configuration, SqlNode contents, String prefix, String prefixesToOverride, String suffix, String suffixesToOverride) {
    //将前后缀配置的字符串转换为对应的集合
    this(configuration, contents, prefix, parseOverrides(prefixesToOverride), suffix, parseOverrides(suffixesToOverride));
  }

  protected TrimSqlNode(Configuration configuration, SqlNode contents, String prefix, List<String> prefixesToOverride, String suffix, List<String> suffixesToOverride) {
    this.contents = contents;
    this.prefix = prefix;
    this.prefixesToOverride = prefixesToOverride;
    this.suffix = suffix;
    this.suffixesToOverride = suffixesToOverride;
    this.configuration = configuration;
  }

  @Override
  public boolean apply(DynamicContext context) {
    //对DynamicContext对象进行包装
    FilteredDynamicContext filteredDynamicContext = new FilteredDynamicContext(context);
    //将包装过的上下文对象传递给子SqlNode节点
    boolean result = contents.apply(filteredDynamicContext);
    filteredDynamicContext.applyAll();
    return result;
  }

  /**
   *  将 a|b|c 类型的字符串转换成对应的字符串(大写)集合
   * @param overrides
   * @return
   */
  private static List<String> parseOverrides(String overrides) {
    if (overrides != null) {
      final StringTokenizer parser = new StringTokenizer(overrides, "|", false);
      final List<String> list = new ArrayList<>(parser.countTokens());
      while (parser.hasMoreTokens()) {
        list.add(parser.nextToken().toUpperCase(Locale.ENGLISH));
      }
      return list;
    }
    return Collections.emptyList();
  }
  /**
   * 继承自DynamicContext类,扩展了DynamicContext类的能力
   *
   */
  private class FilteredDynamicContext extends DynamicContext {
    private DynamicContext delegate;
    //前缀是否已被处理
    private boolean prefixApplied;
    //后缀是否已被处理
    private boolean suffixApplied;
    private StringBuilder sqlBuffer;

    public FilteredDynamicContext(DynamicContext delegate) {
      super(configuration, null);
      this.delegate = delegate;
      this.prefixApplied = false;
      this.suffixApplied = false;
      this.sqlBuffer = new StringBuilder();
    }

    //批量处理前缀和后缀,并将处理过的sql添加到上下文对象中
    public void applyAll() {
      sqlBuffer = new StringBuilder(sqlBuffer.toString().trim());
      //转为大写
      String trimmedUppercaseSql = sqlBuffer.toString().toUpperCase(Locale.ENGLISH);
      if (trimmedUppercaseSql.length() > 0) {
        applyPrefix(sqlBuffer, trimmedUppercaseSql);
        applySuffix(sqlBuffer, trimmedUppercaseSql);
      }
      delegate.appendSql(sqlBuffer.toString());
    }

    @Override
    public Map<String, Object> getBindings() {
      return delegate.getBindings();
    }

    @Override
    public void bind(String name, Object value) {
      delegate.bind(name, value);
    }

    @Override
    public int getUniqueNumber() {
      return delegate.getUniqueNumber();
    }

    @Override
    public void appendSql(String sql) {
      sqlBuffer.append(sql);
    }

    @Override
    public String getSql() {
      return delegate.getSql();
    }
    //处理前缀
    private void applyPrefix(StringBuilder sql, String trimmedUppercaseSql) {
      if (!prefixApplied) {
        prefixApplied = true;
        if (prefixesToOverride != null) {
          for (String toRemove : prefixesToOverride) {
            if (trimmedUppercaseSql.startsWith(toRemove)) {
              sql.delete(0, toRemove.trim().length());//删除sql中对应的前缀
              break;
            }
          }
        }
        //如果前缀不为空,则设置前缀,例如使用<where></where>标签时,
        //mybatis会在所有的条件前面拼接where关键字,从而保证sql语法的正确性
        if (prefix != null) {
          sql.insert(0, " ");
          sql.insert(0, prefix);
        }
      }
    }
    //处理后缀
    private void applySuffix(StringBuilder sql, String trimmedUppercaseSql) {
      if (!suffixApplied) {
        suffixApplied = true;
        if (suffixesToOverride != null) {
          for (String toRemove : suffixesToOverride) {
            if (trimmedUppercaseSql.endsWith(toRemove) || trimmedUppercaseSql.endsWith(toRemove.trim())) {
              int start = sql.length() - toRemove.trim().length();
              int end = sql.length();
              sql.delete(start, end);
              break;
            }
          }
        }
        //如果后缀不为空,则追加后缀
        if (suffix != null) {
          sql.append(" ");
          sql.append(suffix);
        }
      }
    }
  }
}

WhereSqlNode

WhereSqlNode继承自TrimSqlNode类,其对需要忽略的前缀做了定义。WhereSqlNode类的定义如下:

public class WhereSqlNode extends TrimSqlNode {
  //需要忽略的前缀集合
  private static List<String> prefixList = Arrays.asList("AND ","OR ","AND\n", "OR\n", "AND\r", "OR\r", "AND\t", "OR\t");

  public WhereSqlNode(Configuration configuration, SqlNode contents) {
    super(configuration, contents, "WHERE", prefixList, null, null);
  }
}

SetSqlNode

同样,SetSqlNode也是继承自TrimSqlNode类,它也对需要忽略的前缀做了定义。TrimSqlNode类的定义如下:

public class SetSqlNode extends TrimSqlNode {

  private static final List<String> COMMA = Collections.singletonList(",");

  public SetSqlNode(Configuration configuration,SqlNode contents) {
    super(configuration, contents, "SET", COMMA, null, COMMA);
  }
}

SqlNode配置的解析

通过前面的文章可以知道,xxxMapper.xml文件中sql语句的解析是在 XMLStatementBuilder类中完成的,当XMLStatementBuilder类的parseStatementNode方法在解析insert、update、delete和select语句的时候,会触发对sqlNode和sqlSource的解析,入口代码如下:

XMLStatementBuilder#parseStatementNode
在这里插入图片描述

在XMLLanguageDriver#createSqlSource代码如下:

public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) {
  XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType);
  return builder.parseScriptNode();
}

这里通过创建一个XMLScriptBuilder类,并调用其parseScriptNode方法完成对SqlSource的解析和封装

SqlNode的数据结构

SqlNode的数据结构很像一个树结构,MixedSqlNode节点为树枝,其余类型为树叶。其结构大致如下:
在这里插入图片描述

XMLScriptBuilder

查看XMLScriptBuilder类的定义可以发现,XMLScriptBuilde的父类是BaseBuilder类,这说明XMLScriptBuilde和前面介绍的XMLMapperBuilder、XMLStatementBuilder类似,都是通过建造者设计模式来完成对某些配置的解析。

字段

//解析的Xpath节点
private final XNode context;
//当前Xpath节点是否是动态节点
private boolean isDynamic;
private final Class<?> parameterType;
//节点处理器缓存
private final Map<String, NodeHandler> nodeHandlerMap = new HashMap<>();

节点处理器马上会讲

构造函数

public XMLScriptBuilder(Configuration configuration, XNode context) {
  //调用重载的构造函数
  this(configuration, context, null);
}

public XMLScriptBuilder(Configuration configuration, XNode context, Class<?> parameterType) {
  super(configuration);
  //解析的Xpath节点
  this.context = context;
  //参数类型
  this.parameterType = parameterType;
  //初始化节点处理器缓存
  initNodeHandlerMap();
}

初始化节点处理器缓存

初始化节点缓存的方法很简单,就是对每一种sqlNode都创建一个对应的处理器类,并将其保存在HashMap中。

private void initNodeHandlerMap() {
  nodeHandlerMap.put("trim", new TrimHandler());
  nodeHandlerMap.put("where", new WhereHandler());
  nodeHandlerMap.put("set", new SetHandler());
  nodeHandlerMap.put("foreach", new ForEachHandler());
  nodeHandlerMap.put("if", new IfHandler());
  nodeHandlerMap.put("choose", new ChooseHandler());
  nodeHandlerMap.put("when", new IfHandler());
  nodeHandlerMap.put("otherwise", new OtherwiseHandler());
  nodeHandlerMap.put("bind", new BindHandler());
}

parseScriptNode

/**
 * 解析动态标签,并将解析后的结果封装成SqlSource对象返回
 */
public SqlSource parseScriptNode() {
  MixedSqlNode rootSqlNode = parseDynamicTags(context);
  SqlSource sqlSource;
  if (isDynamic) {//如过是动态节点,则返回DynamicSqlSource的实例
    sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
  } else {//否则返回RawSqlSource的实例
    sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
  }
  return sqlSource;
}

parseDynamicTags

用于解析<insert/><update/><select><delete/>内的动态节点

/**
 * 解析动态节点
 * @param node 待解析的xpath节点
 * @return
 */
protected MixedSqlNode parseDynamicTags(XNode node) {
  List<SqlNode> contents = new ArrayList<>();
  //获取当前xpath节点的所有子节点
  NodeList children = node.getNode().getChildNodes();
  for (int i = 0; i < children.getLength(); i++) {
    XNode child = node.newXNode(children.item(i));
    //如果当前节点是文本节点或CDATA
    if (child.getNode().getNodeType() == Node.CDATA_SECTION_NODE || child.getNode().getNodeType() == Node.TEXT_NODE) {
      String data = child.getStringBody("");
      //封装成一个TextSqlNode实例
      TextSqlNode textSqlNode = new TextSqlNode(data);
      //如果是动态节点,则将isDynamic标志位置为true
      if (textSqlNode.isDynamic()) {
        contents.add(textSqlNode);
        isDynamic = true;
      } else {//不是动态节点
        contents.add(new StaticTextSqlNode(data));
      }
      // 如果当前xpath节点还是一个节点  
    } else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) { 
      //获取当前节点的标签名称
      String nodeName = child.getNode().getNodeName();
      //从节点处理器缓存中获取对应的处理器
      NodeHandler handler = nodeHandlerMap.get(nodeName);
      if (handler == null) {
        throw new BuilderException("Unknown element <" + nodeName + "> in SQL statement.");
      }
      //处理当前节点
      handler.handleNode(child, contents);
      isDynamic = true;
    }
  }
  return new MixedSqlNode(contents);
}

节点处理器

XMLScriptBuilder类中定义了一个接口(NodeHandler)和8个实现类,分别用于处理不同类型的SqlNode。他们都是XMLScriptBuilder类的内部类,其类图如下所示:
在这里插入图片描述

NodeHandler

该接口只定义了一个方法,该方法用于解析对应的SqlNode

  /**
   *  节点处理器接口
   */
  private interface NodeHandler {
    /**
     * 处理节点
     * @param nodeToHandle 待处理的Xpath节点
     * @param targetContents 已解析的SqlNode集合
     */
    void handleNode(XNode nodeToHandle, List<SqlNode> targetContents);
  }
IfHandler

用于解析<if></if>节点的配置

private class IfHandler implements NodeHandler {
  public IfHandler() {
    // Prevent Synthetic Access
  }

  @Override
  public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
    //解析动态标签,获取树枝节点
    MixedSqlNode mixedSqlNode = parseDynamicTags(nodeToHandle);
    //获取配置的test属性
    String test = nodeToHandle.getStringAttribute("test");
    //构建IfSqlNode实例
    IfSqlNode ifSqlNode = new IfSqlNode(mixedSqlNode, test);
    targetContents.add(ifSqlNode);
  }
}
BindHandler

用于解析<bing></bing>节点的配置,直接读取配置信息创建VarDeclSqlNode对象即可

private class BindHandler implements NodeHandler {
  public BindHandler() {
    // Prevent Synthetic Access
  }

  @Override
  public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
    final String name = nodeToHandle.getStringAttribute("name");
    final String expression = nodeToHandle.getStringAttribute("value");
    final VarDeclSqlNode node = new VarDeclSqlNode(name, expression);
    targetContents.add(node);
  }
}
TrimHandler

用于解析<trim></trim>节点的配置,直接读取配置信息创建TrimSqlNode对象

private class TrimHandler implements NodeHandler {
  public TrimHandler() {
    // Prevent Synthetic Access
  }

  @Override
  public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
    //解析动态标签,获取树枝节点
    MixedSqlNode mixedSqlNode = parseDynamicTags(nodeToHandle);
    String prefix = nodeToHandle.getStringAttribute("prefix");
    String prefixOverrides = nodeToHandle.getStringAttribute("prefixOverrides");
    String suffix = nodeToHandle.getStringAttribute("suffix");
    String suffixOverrides = nodeToHandle.getStringAttribute("suffixOverrides");
    TrimSqlNode trim = new TrimSqlNode(configuration, mixedSqlNode, prefix, prefixOverrides, suffix, suffixOverrides);
    targetContents.add(trim);
  }
}
WhereHandler

用于解析<where></where>节点的配置,直接读取配置信息创建WhereSqlNode对象

private class WhereHandler implements NodeHandler {
  public WhereHandler() {
    // Prevent Synthetic Access
  }
  @Override
  public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
    //解析动态标签,获取树枝节点
    MixedSqlNode mixedSqlNode = parseDynamicTags(nodeToHandle);
    WhereSqlNode where = new WhereSqlNode(configuration, mixedSqlNode);
    targetContents.add(where);
  }
}
SetHandler

用于解析<set></set>节点的配置

private class SetHandler implements NodeHandler {
  public SetHandler() {
    // Prevent Synthetic Access
  }

  @Override
  public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
    MixedSqlNode mixedSqlNode = parseDynamicTags(nodeToHandle);
    SetSqlNode set = new SetSqlNode(configuration, mixedSqlNode);
    targetContents.add(set);
  }
}
ForEachHandler

用于解析<foreach></foreach>节点的配置

private class ForEachHandler implements NodeHandler {
  public ForEachHandler() {
    // Prevent Synthetic Access
  }

  @Override
  public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
    MixedSqlNode mixedSqlNode = parseDynamicTags(nodeToHandle);
    String collection = nodeToHandle.getStringAttribute("collection");
    String item = nodeToHandle.getStringAttribute("item");
    String index = nodeToHandle.getStringAttribute("index");
    String open = nodeToHandle.getStringAttribute("open");
    String close = nodeToHandle.getStringAttribute("close");
    String separator = nodeToHandle.getStringAttribute("separator");
    ForEachSqlNode forEachSqlNode = new ForEachSqlNode(configuration, mixedSqlNode, collection, index, item, open, close, separator);
    targetContents.add(forEachSqlNode);
  }
}
ChooseHandler
private class ChooseHandler implements NodeHandler {
  public ChooseHandler() {
    // Prevent Synthetic Access
  }

  @Override
  public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
    //when标签对应的SqlNode的集合
    List<SqlNode> whenSqlNodes = new ArrayList<>();
    //otherwise标签对应的SqlNode的集合
    List<SqlNode> otherwiseSqlNodes = new ArrayList<>();
    //处理 when 和 otherwise 节点
    handleWhenOtherwiseNodes(nodeToHandle, whenSqlNodes, otherwiseSqlNodes);
    //获取otherwise节点
    SqlNode defaultSqlNode = getDefaultSqlNode(otherwiseSqlNodes);
    //创建ChooseSqlNode对象
    ChooseSqlNode chooseSqlNode = new ChooseSqlNode(whenSqlNodes, defaultSqlNode);
    targetContents.add(chooseSqlNode);
  }
  //迭代集合,对集合中的每一个XNode进行处理
  private void handleWhenOtherwiseNodes(XNode chooseSqlNode, List<SqlNode> ifSqlNodes, List<SqlNode> defaultSqlNodes) {
    List<XNode> children = chooseSqlNode.getChildren();
    for (XNode child : children) {
      String nodeName = child.getNode().getNodeName();
      NodeHandler handler = nodeHandlerMap.get(nodeName);
      if (handler instanceof IfHandler) {
        handler.handleNode(child, ifSqlNodes);
      } else if (handler instanceof OtherwiseHandler) {
        handler.handleNode(child, defaultSqlNodes);
      }
    }
  }
  //将集合转为对象,并校验用户的配置是否合法
  private SqlNode getDefaultSqlNode(List<SqlNode> defaultSqlNodes) {
    SqlNode defaultSqlNode = null;
    if (defaultSqlNodes.size() == 1) {
      defaultSqlNode = defaultSqlNodes.get(0);
    } else if (defaultSqlNodes.size() > 1) {
      throw new BuilderException("Too many default (otherwise) elements in choose statement.");
    }
    return defaultSqlNode;
  }
}
  • 5
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值