转载

MyBatis 的秘密(八)动态SQL

动态SQL

说到动态 SQL ,就不得不提 Script , Java 作为一个静态语音,代码需要先编译,然后再运行,虽然带来了效率,但是却损失了灵活性。

Spring 为此还专门提供了一套 SpEL 用来封装 Java 脚本语言 API

MyBatis 中,也支持动态 SQL ,想要将简单的 String 字符串编译成能运行的代码,需要其他的库的支持, MyBatis 内部使用的是 OGNL 库。

OgnlCache 中,是 MyBatisOGNL 的简单封装:

public static Object getValue(String expression, Object root) {
    try {
        Map context = Ognl.createDefaultContext(root, MEMBER_ACCESS, CLASS_RESOLVER, null);
        return Ognl.getValue(parseExpression(expression), context, root);
    } catch (OgnlException e) {
        throw new BuilderException("Error evaluating expression '" + expression + "'. Cause: " + e, e);
    }
}

主要便是增加了一层缓存。

有了上面的基础,我们就可以通过需求,来了解实现了:

MyBatis 中,动态 SQL 标签有如下几个:

  • if :通过条件判断执行 SQL
  • choose :通过 switch 选择一条执行 SQL 一般和 when / otherwise 一起使用
  • trim : 简单加工 SQL ,比如去除头尾的逗号等,同类的还有 where / set
  • foreach : 遍历容器,将遍历的结果拼接成 SQL
  • bind : 通过 OGNL 表达式获取指定的值,并绑定到环境变量中

简单的使用方式如下:

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

可以看到,动态 SQL 的关键就是获取 title 的值,然后执行 test 对应的表达式,最后根据结果拼接 SQL

最后也是比较重要的一点就是, MyBatis 的动态 SQL 标签是可以嵌套使用的:

比如:

<update id="update" parameterType="User">
   UPDATE users
   <trim prefix="SET" prefixOverrides=",">
       <if test="name != null and name != ''">
           name = #{name}
       </if>
       <if test="age != null and age != ''">
           , age = #{age}
       </if>
       <if test="birthday != null and birthday != ''">
           , birthday = #{birthday}
       </if>
   </trim>
   <where> 1=1
     <if test="id != null">
       and id = ${id}
     </if>
   </where>
</update>

这样的结构,就像是一颗树,需要层层遍历处理。

组合模式

前面说到了 MyBatis 处理动态 SQL 的需求,需要处理嵌套的标签。

而这个,恰好符合组合模式的解决场景。

MyBatis 中,处理动态 SQL 的关键类如下:

  • SqlNode : 用来表示动态标签的相关信息
  • NodeHandler : 用来处理 SqlNode 其他信息的类
  • DynamicContext : 用来保存处理整个标签过程中,解析出来的信息,主要元素为 StringBuilder
  • SqlSource : 用来表示 XMLSQL 的信息, MyBatis 中,动态 SQL 最终都会通过 SqlSource 表示

SqlNode 接口的定义如下:

public interface SqlNode {
  //处理目前的信息,并将处理完毕的信息追加到DynamicContext 中  
  boolean apply(DynamicContext context);
}

接下来从 MyBaits 创建以及使用 SqlSource 上来分析动态 SQL 的使用:

创建 SqlSource 的代码如下:

XMLScriptBuilder#parseScriptNode()

public SqlSource parseScriptNode() {
    //创建组合模式中的根节点
    MixedSqlNode rootSqlNode = parseDynamicTags(context);
    SqlSource sqlSource;
    //如果发现是动态节点,则创建DynamicSqlSource
    //反之创建RawSqlSource
    if (isDynamic) {
        sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
    } else {
        sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
    }
    return sqlSource;
}

接着看 parseDynamicTags()

protected MixedSqlNode parseDynamicTags(XNode node) {
    //使用list保存所有sqlNode  
    List<SqlNode> contents = new ArrayList<>();
    //遍历所有的子节点  
    NodeList children = node.getNode().getChildNodes();
    for (int i = 0; i < children.getLength(); i++) {
      XNode child = node.newXNode(children.item(i));
      //如果节点是Text节点,则使用TextSqlNode处理
      if (child.getNode().getNodeType() == Node.CDATA_SECTION_NODE || child.getNode().getNodeType() == Node.TEXT_NODE) {
        String data = child.getStringBody("");
        TextSqlNode textSqlNode = new TextSqlNode(data);
        //包含${},则需要额外处理
        if (textSqlNode.isDynamic()) {
          contents.add(textSqlNode);
          isDynamic = true;
        } else {
          contents.add(new StaticTextSqlNode(data));
        }
      }
      //如果是一个节点
      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);
  }

TextSqlNode 作用之一便是检测 SQL 中是否包含 ${} ,如果包含,则判断为 Dynamic

TextSqlNode 的作用主要和 #{xxx} 类似,但是实现方式不同, #{xxx} 底层是通过 JDBC#ParperedStatementsetXXX 方法设置参数,具有防止 SQL 注入的功能,而 TextSqlNode 则是直接替换的 String ,不会做任何的 SQL 处理,因此一般不建议使用。

接下来再看 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) {
    //遍历调用apply方法  
    contents.forEach(node -> node.apply(context));
    return true;
  }
}

可以看见,非常简单,就是用来遍历所有子节点,分别调用 apply() 方法。

接下来我们看看其他标签的使用:

IfSqlNode

首先看 ifSqlNode 的创建:

IfSqlHandler#handleNode()

@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);
}

可以看到,这里 IfNode 也充当了一个根节点,里面包含了其子节点信息。

这里可以大概猜想处理, IfSqlNode 会通过 OGNL 执行 test 的内容,如果 true ,则执行后面的 SqlNode ,否则跳过

IfSqlNode#apply()

@Override
  public boolean apply(DynamicContext context) {
    //通过OGNL判断test的值  
    if (evaluator.evaluateBoolean(test, context.getBindings())) {
      //如果为`true`则遍历子节点执行
      contents.apply(context);
      return true;
    }
    //否则跳过  
    return false;
  }

可以看到和前面的推理相符合

ChooseNode

ChooseHandler#handleNode()

@Override
public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
    List<SqlNode> whenSqlNodes = new ArrayList<>();
    List<SqlNode> otherwiseSqlNodes = new ArrayList<>();
    //遍历子节点,生成对应的SqlNode 将其保存在各个对应的容器中
    //whenSqlNode 的处理和IfNode的处理相同
    handleWhenOtherwiseNodes(nodeToHandle, whenSqlNodes, otherwiseSqlNodes);
    //验证otherwise的数量的合法性,只能有一个otherwise节点
    SqlNode defaultSqlNode = getDefaultSqlNode(otherwiseSqlNodes);
    //生成对应的ChooseSqlNode
    ChooseSqlNode chooseSqlNode = new ChooseSqlNode(whenSqlNodes, defaultSqlNode);
    targetContents.add(chooseSqlNode);
}

这里就可以猜想到 ChooseNodeNode 的处理的,应该是遍历所有的 ifNode ,然后当遇到符合条件的,边处理后续的 Node ,否则执行 otherwise

ChooseSqlNode#apply()

@Override
  public boolean apply(DynamicContext context) {
    for (SqlNode sqlNode : ifSqlNodes) {
      if (sqlNode.apply(context)) {
        return true;
      }
    }
    if (defaultSqlNode != null) {
      defaultSqlNode.apply(context);
      return true;
    }
    return false;
  }

TrimNode

TrimeNode 是对 SQL 语句进行加工。

其包含3个属性:

  • prefix : 需要添加的前缀
  • suffix : 需要添加的尾缀
  • prefixOverrides : 当 SQL 是以此标志开头的时候,需要移除的开头的内容
  • suffixOverrides : 当 SQL 是以此标志结尾的时候,需要移除的结尾的内容

现在举个例子:

<select id="findActiveBlogLike"
     resultType="Blog">
  SELECT * FROM BLOG
    <trim prefix="WHERE" prefixOverrides="AND |OR ">
        <if test="state != null">
            state = #{state}
        </if>
        <if test="title != null">
            AND title like #{title}
        </if>
    </trim>
</select>

可以看到, trim 会自动为 SQL 增加 Where 前缀,同时当 statenull 的时候, SQL 会以 AND 开头,此时 trim 标签便会自动将 AND 删除。

同理, SET 可能会遇到 , 结尾,只需要使用 suffixOverrides 删除结尾即可,这里不再叙述。

接下来查看 Trim 的源码:

TrimHandler#handleNode()

@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`
    TrimSqlNode trim = new TrimSqlNode(configuration, mixedSqlNode, prefix, prefixOverrides, suffix, suffixOverrides);
    targetContents.add(trim);
}

这里可以看到,没有其他的处理,只是获取了属性然后初始化

TrimSqlNode#apply()

@Override
public boolean apply(DynamicContext context) {
    //创建FilteredDynamicContext对象
    FilteredDynamicContext filteredDynamicContext = new FilteredDynamicContext(context);
    //获取子元素的处理结果
    boolean result = contents.apply(filteredDynamicContext);
    //整体拼接SQL
    filteredDynamicContext.applyAll();
    return result;
}

这里出现了一个新的对象: FilteredDynamicContext , FilteredDynamicContext 继承自 DynamicContext ,其相对于 DynamicContext 仅仅多了一个新的方法: applyAll() ,

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());
}

其中, applyPrefix() 方法会检查 SQL 是否 startWith() 需要删除的元素,如果有,则删除。

private void applyPrefix(StringBuilder sql, String trimmedUppercaseSql) {
      if (!prefixApplied) {
        prefixApplied = true;
        if (prefixesToOverride != null) {
          for (String toRemove : prefixesToOverride) {
            //如果SQL以toRemove开头,则删除
            if (trimmedUppercaseSql.startsWith(toRemove)) {
              sql.delete(0, toRemove.trim().length());
              break;
            }
          }
        }
        if (prefix != null) {
          sql.insert(0, " ");
          sql.insert(0, prefix);
        }
      }
    }

ForEachNode

foreach 节点的元素很多:

  • item : 遍历的时候所获取的元素的具体的值,类似 for(String item:list ) 中的 item ,对于 Map , item 对应为 value
  • index : 遍历的时候所遍历的索引,类似 for(int i=0;i<10;i++) 中的 i ,对于 Mapindex 对应为 key

  • collection : 需要遍历的集合的参数名字,如果指定了 @Param ,则名字为 @Param 指定的名字,否则如果只有一个参数,且这个参数是集合的话,需要使用 MyBatis 包装的名字:

    • 对于 Collection : 名字为 collection
    • 对于 List : 名字为 list
    • 对于数组:名字为 array

    相关代码如下:

    private Object wrapCollection(final Object object) {
      if (object instanceof Collection) {
        StrictMap<Object> map = new StrictMap<>();
        map.put("collection", object);
        if (object instanceof List) {
          map.put("list", object);
        }
        return map;
      } else if (object != null && object.getClass().isArray()) {
        StrictMap<Object> map = new StrictMap<>();
        map.put("array", object);
        return map;
      }
      return object;
    }
  • open : 类似 TrimNode 中的 prefix
  • close : 类似 TrimNode 中的 suffix

  • separator : 每个 SQL 的分割符

使用方式如下:

<select id="selectPostIn" resultType="domain.blog.Post">
  SELECT *
  FROM POST P
  WHERE ID in
  <foreach item="item" index="index" collection="list"
      open="(" separator="," close=")">
        #{item}
  </foreach>
</select>

以上元素没有默认值,当没有设置的时候, MyBatis 便不会设置相关的值,对于 openclose ,我们一般都会自己加上括号,所以有时候可以不设置。

接下来我们查看 MyBatisforeach 的源码:

ForEachNode 的初始化代码没什么好看的,就是简单的获取相关的属性,然后初始化。我们直接看其 apply() 方法。

public 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;
    //追加Open符号
    applyOpen(context);
    //记录索引,用来赋值给`index`
    int i = 0;
    //调用`OGNL`的迭代器
    for (Object o : iterable) {
            //PrefixedContext继承自DynamicContext,主要是增加了分隔符
            context = new PrefixedContext(context, "");
        } else {
            context = new PrefixedContext(context, separator);
        }
        int uniqueNumber = context.getUniqueNumber();
        // Issue #709
        //对于Map key会绑定到index , value会绑定到item上
        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 {
            //实时绑定i到index上
            applyIndex(context, i, uniqueNumber);
            //实时绑定具体的值到item上
            applyItem(context, o, uniqueNumber);
        }
        //生成对应的占位符,并绑定相关的值#{__frch_item_1}等
        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;
}

BindNode

bind 节点可以方便的运行 OGNL 表达式,并将结果绑定到指定的变量。

使用方法如下:

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

一般可以内置使用的元素为 _parameter 表示现在的参数,以及 _databaseId ,表示现在的 database id

对于 BindNode ,对应的是 VarDeclSqlNode ,具体的代码这里不再细看,大概就是使用 OGNL 获取具体的值,比较简单。

对于动态 SQL 的节点对应的类,我们就分析完了,可以看到 SqlNode 完美的应用了组合模式,每个 SqlNode 都保存了其子节点下面的节点,执行下来便像是一颗树的递归。

当然, SqlNode 的使用仅仅是动态 SQL 的一部分,但是它确实动态 SQL 的核心部分。

原文  http://dengchengchao.com/?p=1211
正文到此结束
Loading...