Mybatis Sql 语句解析之 Include 节点的替换

307 阅读3分钟

Mybatis Sql 语句解析之 Include 节点的替换

做为一名菜鸟程序员,第一次读源码,想做一些记录和分享一下自己读源码的思维和成果,但是因为自己的能力有限,所以还有很多不足和错误的地方,希望大家多批评和指正,共同进步。

解析 sql 语句之前,先看下 mapper/sql 节点是如何解析的

// 解析 sql 节点
sqlElement(context.evalNodes("/mapper/sql"));
// 解析 select/insert/update/delete 节点 
buildStatementFromContext(context.evalNodes("select|insert|update|delete"));

sql 节点解析

private void sqlElement(List<XNode> list, String requiredDatabaseId) {
    for (XNode context : list) {
        String databaseId = context.getStringAttribute("databaseId");
            // 首先获取 sql#id 属性
        String id = context.getStringAttribute("id");
        // 获取完整的 id 全局名称 id = namespace.id
        id = builderAssistant.applyCurrentNamespace(id, false);
        if (databaseIdMatchesCurrent(id, databaseId, requiredDatabaseId)) {
            // 把 id 与 sql 节点放入 sqlFragments 中
            // 注意:因为 sqlFragments 是 Map 类型,传入的是 Configuration 的 sqlFragments 字段,所以 put 到了 Configuration#sqlFragments 字段里
            sqlFragments.put(id, context);
        }
    }
}

Sql 语句的 Include 节点替换

// 主要看 parseStatementNode 方法中一下代码块
private final MapperBuilderAssistant builderAssistant;
// select|insert|update|delete 节点
private final XNode context;
private final String requiredDatabaseId;
public void parseStatementNode() {
    XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
    // Include 替换的核心逻辑
    includeParser.applyIncludes(context.getNode());
}
private void applyIncludes(Node source, final Properties variablesContext, boolean included) {
    // 解析 sql 中的 include 节点
    if (source.getNodeName().equals("include")) { // <1>
        // source = <include refid="baseSql" />
        // 通过 <select> <include refid = "baseSql"> </select>
        // 获取到如下节点
        // <sql id = "baseSql"> 节点内容 </sql>
        Node toInclude = findSqlFragment(getStringAttribute(source, "refid"), variablesContext);
        // 这里的 include 节点可能存在嵌套节点
        Properties toIncludeContext = getVariablesContext(source, variablesContext);
        // 递归:<include refid = "baseSql" /      // 替换成 <sql id = "baseSql" > id, name </sql
        // 最终替换成 id, name
        applyIncludes(toInclude, toIncludeContext, true);
        if (toInclude.getOwnerDocument() != source.getOwnerDocument()) {
            toInclude = source.getOwnerDocument().importNode(toInclude, true);
        }
        source.getParentNode().replaceChild(toInclude, source); // <1.1> 把 <include /> 替换成 <sql/>
        while (toInclude.hasChildNodes()) {
            toInclude.getParentNode().insertBefore(toInclude.getFirstChild(), toInclude); // <1.2> 把 <sql/> 的第一个子节点,也就是 text 节点,添加到 <sql/> 前边
        }
        toInclude.getParentNode().removeChild(toInclude); // 然后再删除 <sql/> 节点
    } else if (source.getNodeType() == Node.ELEMENT_NODE) { // <2> 
        if (included && !variablesContext.isEmpty()) { // <2.1>
            // replace variables in attribute values
            NamedNodeMap attributes = source.getAttributes();
            for (int i = 0; i < attributes.getLength(); i++) {
              Node attr = attributes.item(i);
              attr.setNodeValue(PropertyParser.parse(attr.getNodeValue(), variablesContext)); // 此时如果 nodeValue 内容是 ${xx},则进行替换
            }
        }
        NodeList children = source.getChildNodes(); // <2.2>
        for (int i = 0; i < children.getLength(); i++) {
            applyIncludes(children.item(i), variablesContext, included); // <2.3>
        }
    } else if (included && (source.getNodeType() == Node.TEXT_NODE || source.getNodeType() == Node.CDATA_SECTION_NODE) // 这里是解析 text 文本节点的
               && !variablesContext.isEmpty()) {
      // replace variables in text node
        source.setNodeValue(PropertyParser.parse(source.getNodeValue(), variablesContext));
    }
}
  1. 第一次进入该方法时 source 参数的节点类型是 select,所以会执行代码 <2>

  2. 因为 included == false,所以跳过代码 <2.1>

  3. 此时执行 代码 <2.2>,假设当前 sql 是 select 语句,且语句如下:

    <!-- sql1 -->
    <sql id="baseSql">
      id,
      name
    </sql>
    <!-- sql2 -->
    <select id="findById" parameterType="integer" resultType="Article">
      select
    	<include refid="baseSql"/>
      from article
      where id = #{id}
    </select>
    
  4. 获取 source(findById) 的子节点,包含三个子节点:

    1. 第一个子节点 text 类型 :select
    2. 第二个子节点 include 节点:
    3. 第三个子节点 text 类型:where id = #{id}
  5. 此时在代码 <2.3> 递归到第一个子节点,因为无需处理跳过,

  6. 第二次递归<2.3> 执行第二个子节点,因为节点名称是 include,所以进入代码<1>

  7. 在 findSqlFragment 方法中,查询 id = basesql 的 sql 代码块,如果查找不到,则放到不完整的元素代码后,后续在进行处理。

  8. 然后在代码<1.1> 中把 source.getParentNode 节点也就是 select 节点中的 节点进行替换,替换为 节点

  9. 此时在 toInclude 属性是包含一个 text 子节点,在代码<1.2> 把 text 子节点添加到 节点中

  10. 在移除 toInclude 节点,此时 parent 节点下 包含 3 个 text 文本节点。

  11. 第三次递归 <2.3> 代码,因为是 text 文本节点跳过

  12. 此时 sql2 的 sql 语句已经被替换成

    <select id="findById" parameterType="integer" resultType="Article">
      select
    	id, name
      from article
      where id = #{id}
    </select>
    
  13. 替换完成

  14. 疑惑:在第 9、10 步中为什么不进行替换,而是先增加 text 文本节点,再删除 sql 节点。