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));
}
}
-
第一次进入该方法时 source 参数的节点类型是 select,所以会执行代码 <2>
-
因为 included == false,所以跳过代码 <2.1>
-
此时执行 代码 <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>
-
获取 source(findById) 的子节点,包含三个子节点:
- 第一个子节点 text 类型 :select
- 第二个子节点 include 节点:
- 第三个子节点 text 类型:where id = #{id}
-
此时在代码 <2.3> 递归到第一个子节点,因为无需处理跳过,
-
第二次递归<2.3> 执行第二个子节点,因为节点名称是 include,所以进入代码<1>
-
在 findSqlFragment 方法中,查询 id = basesql 的 sql 代码块,如果查找不到,则放到不完整的元素代码后,后续在进行处理。
-
然后在代码<1.1> 中把 source.getParentNode 节点也就是 select 节点中的 节点进行替换,替换为 节点
-
此时在 toInclude 属性是包含一个 text 子节点,在代码<1.2> 把 text 子节点添加到 节点中
-
在移除 toInclude 节点,此时 parent 节点下 包含 3 个 text 文本节点。
-
第三次递归 <2.3> 代码,因为是 text 文本节点跳过
-
此时 sql2 的 sql 语句已经被替换成
<select id="findById" parameterType="integer" resultType="Article"> select id, name from article where id = #{id} </select>
-
替换完成
-
疑惑:在第 9、10 步中为什么不进行替换,而是先增加 text 文本节点,再删除 sql 节点。