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

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 节点。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值