MyBatis映射器文件解析:动态SQL语句大家好,我是王有志。今天我们来学习 MyBatis 应用程序解析动态 SQL
XMLScriptBuilder#parseDynamicTags 方法分析
protected MixedSqlNode parseDynamicTags(XNode node) {
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));
if (child.getNode().getNodeType() == Node.CDATA_SECTION_NODE || child.getNode().getNodeType() == Node.TEXT_NODE) {
// 处理静态 SQL 语句,省略
} else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) {
// 处理动态 SQL 语句
String nodeName = child.getNode().getNodeName();
NodeHandler handler = nodeHandlerMap.get(nodeName);
handler.handleNode(child, contents);
isDynamic = true;
}
}
return new MixedSqlNode(contents);
}
首先我们来明确下 XMLScriptBuilder#parseDynamicTags
方法的入参,XNode 实例是我们在 MyBatis 映射器中配置的每一条完整的 SQL 语句,如下:第 2 行代码,创建了 List 容器 contents,用于存储生成的 SqlNode 实例。第 3 行代码,用于获取该 SQL 语句中的所有子元素,注意这里的子元素不仅仅是图中所示的 where 元素等,还包括图中展示的文本元素,如:
select * from order_item
。那么 NodeList 中会有多少子元素呢?如上图所示的 SQL 语句中子元素会有 3 个:select * from order_item
,where 元素以及一个回车符,那么我们可以知道 Node#getChildNodes
方法并不会递归获取子元素的子元素,另外后面我们会忽略这个回车符。接下来进入第 4 行的循环语句,我们看针对 NodeList 中的元素的处理:
下面我们着重分析第 11 行代码和第 12 行代码中出现的对象和方法进行分析。
XMLScriptBuilder#initNodeHandlerMap 方法
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());
}
源码非常简单,只是向容器 nodeHandlerMap 中添加 NodeHandler 实例,其中 Key 是 MyBatis 映射器中提供的动态元素的名称,而 Value 则是不同动态元素的 NodeHandler 实现类的实例。这里注意,MyBatis 提供的用于实现动态 SQL 语句的元素有 9 个,而对应的 NodeHandler 实现类总共有 8 个,这是因为 if 元素与 where 元素共用了实现类 IfHandler。
NodeHandler 的结构
NodeHandler 是 XMLScriptBuilder 定义的内部接口,并且 NodeHandler 的实现类也都是 XMLScriptBuilder 的内部类。我理解 MyBatis 将 NodeHandler 和 NodeHandler 的实现类定义为 XMLScriptBuilder 的内部接口和内部类是因为它们的功能不会扩散出 XMLScriptBuilder,因此只需要定义在 XMLScriptBuilder 内部即可。XMLScriptBuilder 有 8 个实现类,如下:NodeHandler 接口中只定义了一个方法:
private interface NodeHandler {
void handleNode(XNode nodeToHandle, List<SqlNode> targetContents);
}
而 NodeHandler 的实现类在实现 NodeHandler#handleNode
方法时,没有进行特别的处理,只是解析对应元素中的属性,并生成相应的 SqlNode,这里我以最常见的 IfHandler 为例进行说明,源码如下:
private class IfHandler implements NodeHandler {
public IfHandler() {
}
@Override
public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
MixedSqlNode mixedSqlNode = parseDynamicTags(nodeToHandle);
String test = nodeToHandle.getStringAttribute("test");
IfSqlNode ifSqlNode = new IfSqlNode(mixedSqlNode, test);
targetContents.add(ifSqlNode);
}
}
可以看到在 IfHandler#handleNode
方法中,首先调用了 XMLScriptBuilder#parseDynamicTags
方法,这是因为大部分动态 SQL 语句的元素中都支持嵌套子元素,而 Node#getChildNodes
方法无法获取再次嵌套的子元素,因此在支持嵌套子元素的动态 SQL 语句元素中,要先调用 XMLScriptBuilder#parseDynamicTags
方法处理嵌套的子元素,这里结合 XMLScriptBuilder#parseDynamicTags
方法的调用逻辑,可以看到这里也是一个递归调用。Tips:在所有 NodeHandler 的实现类中,只有 BindHandler 和 ChooseHandler 中没有调用 XMLScriptBuilder#parseDynamicTags
方法。第 8 行代码,获取了 if 元素的 test 属性,并在第 9 行代码中构建了 IfSqlNode 实例。第 10 行代码,将 IfSqlNode 实例保存到 XMLScriptBuilder#parseDynamicTags
方法中创建的变量 contents 中。第 9 行代码,创建了 if 元素对应的 SqlNode 实例 IfSqlNode,构造方法很简单,这里就不和大家一起看了。第 10 行代码,将创建的 IfSqlNode 实例添加到 targetContents 中,这里的 targetContents 就是在 XMLScriptBuilder#parseDynamicTags
方法中创建的 contents。
SqlNode 的结构
SqlNode 是 MyBatis 中定义的接口,它只定义了一个方法:
public interface SqlNode {
boolean apply(DynamicContext context);
}
该方法会根据传入参数,解析 SqlNode 记录的动态 SQL 语句,这个方法我们会在 MyBatis 应用程序执行 SQL 语句的内容中再深入的了解。SqlNode 有 10 个实现类,如下:下面我们就来了解每个 SqlNode 的实现类的类型声明和构造方法。
MixedSqlNode
MixedSqlNode 的类型声明,成员变量和构造方法如下:
public class MixedSqlNode implements SqlNode {
private final List<SqlNode> contents;
public MixedSqlNode(List<SqlNode> contents) {
this.contents = contents;
}
}
MixedSqlNode 是一条 MyBatis 映射器中 SQL 语句解析后的所有 SqlNode 实例的集合,它使用 contents 字段记录了所有 SQL 语句中子元素对应的 SqlNOde 实例。
StaticTextSqlNode
StaticTextSqlNode 的类型声明,成员变量和构造方法如下:
public class StaticTextSqlNode implements SqlNode {
private final String text;
public StaticTextSqlNode(String text) {
this.text = text;
}
}
StaticTextSqlNode 中使用 text 字段记录了非动态 SQL 语句的内容,MyBatis 认为不含有 trim 元素,where 元素,set 元素,foreach 元素, if 元素,choose 元素,when 元素,otherwise 元素和 bind 元素的 SQL 语句都是非动态 SQL 语句。
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;
}
}
与 StaticTextSqlNode 类似,TextSqlNode 中使用了 text 字段记录了非动态 SQL 语句的内容,但它们的差别是,当静态 SQL 语句中出现了占位符“${}”时,使用 TextSqlNode,其余情况使用 StaticTextSqlNode。
IfSqlNode
IfSqlNode 的类型声明,成员变量和构造方法如下:
public class IfSqlNode implements SqlNode {
private final ExpressionEvaluator evaluator;
private final String test;
private final SqlNode contents;
public IfSqlNode(SqlNode contents, String test) {
this.test = test;
this.contents = contents;
this.evaluator = new ExpressionEvaluator();
}
}
IfSqlNode 用于处理动态 SQL 语句的 if 元素,它声明了 3 个成员变量:
- String 类型的 test 字段,用于存储 if 元素中 test 属性的内容,该属性中配置的是 OGNL 表达式;
- ExpressionEvaluator 类型的 evaluator 字段,表达式计算器,用于处理 if 元素中的 OGNL 表达式;
- SqlNode 类型的 contents 字段,用于记录 if 元素的子元素。
ForEachSqlNode
ForEachSqlNode 的类型声明,成员变量和构造方法如下:
public class ForEachSqlNode implements SqlNode {
public static final String ITEM_PREFIX = "__frch_";
private final ExpressionEvaluator evaluator;
private final String collectionExpression;
private final Boolean nullable;
private final SqlNode contents;
private final String open;
private final String close;
private final String separator;
private final String item;
private final String index;
private final Configuration configuration;
public ForEachSqlNode(Configuration configuration, SqlNode contents, String collectionExpression, Boolean nullable, String index, String item, String open, String close, String separator) {
this.evaluator = new ExpressionEvaluator();
this.collectionExpression = collectionExpression;
this.nullable = nullable;
this.contents = contents;
this.open = open;
this.close = close;
this.separator = separator;
this.index = index;
this.item = item;
this.configuration = configuration;
}
}
ForEachSqlNode 用于处理动态 SQL 语句中的 foreach 元素,它声明了 10 个成员变量:
- ExpressionEvaluator 的 evaluator,表达式计算器,用于计算 foreach 元素的终止条件;
- String 的 collectionExpression,循环迭代中使用的集合,对应 foreach 元素中的 collection 属性;
- Boolean 的 nullable,表示是否允许出现 NULL,对应 foreach 元素中的 nullable 属性;
- SqlNode 的 contents,用于记录 foreach 元素的子元素;
- String 的 open,要在循环开始前添加的字符串,对应 foreach 元素中的 open 属性;
- String 的 close,要在循环结束后添加的字符串,对应 foreach 元素中的 close 属性;
- String 的 separator,集合中每项元素时之间添加的分隔符,对应 foreach 元素中的 separator 属性;
- String 的 item,当前循环中迭代的元素,如果 foreach 元素循环迭代的是 Map,此时为 Value;
- String 的 index,当前迭代的次数,如果 foreach 元素循环迭代的是 Map,此时为 Key;
- Configuration 的 configuration,MyBatis 核心配置文件在 MyBatis 应用程序中的映射。
TrimSqlNode
TrimSqlNode 的类型声明,成员变量和构造方法如下:
public class TrimSqlNode implements 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;
}
}
TrimSqlNode 用于处理动态 SQL 语句的 trim 元素,它声明了 6 个成员变量:
- String 类型的 prefix,需要添加的指定前缀,对应 trim 元素的 prefix 属性;
- String 类型的 suffix,需要添加的指定后缀,对应 trim 元素的 suffix 属性;
- List\ <\String> 类型的 prefixesToOverride,需要删除的指定前缀,对应 trim 元素的 prefixOverrides 属性;
- List\ <\String> 类型的 suffixesToOverride,需要删除的指定后缀,对应 trim 元素的 suffixOverrides 属性;
- SqlNode 类型的 contents,用于记录 trim 元素的子元素;
- Configuration 类型的 configuration,MyBatis 核心配置文件在 MyBatis 应用程序中的映射。
通过 trim 元素生成的 SQL 语句片段,会在生成的 SQL 语句片段前后删除 prefixOverrides 字段和 suffixOverrides 字段配置的内容,并在生成的 SQL 语句片段前后添加 prefix 字段和 suffix 字段配置的内容,这点我们会在后面聊到 TrimSqlNode#apply
方法时再和大家详细分享实现过程。
SetSqlNode
SetSqlNode 的类型声明,成员变量和构造方法如下:
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);
}
}
SetSqlNode 继承自 TrimSqlNode,用于处理动态 SQL 语句中的 set 元素。SetSqlNode 指定了成员变量 prefix 字段为 “SET”,suffixesToOverride 字段为“,”,suffix 字段和 prefixesToOverride 字段为 null。也就是说,使用 set 元素生成的 SQL 语句片段中,如果以“,”结尾,MyBatis 会主动删除“,”,并在 SQL 语句片段的开头添加上“SET”。例如:
<update id="updateByItemId" parameterType="com.wyz.entity.OrderItemDO">
update order_item
<set>
<if test="orderItem.commodityId != null">
commodity_id = #{orderItem.commodityId, jdbcType=INTEGER},
</if>
<if test="orderItem.commodityPrice != null">
commodity_price = #{orderItem.commodityPrice, jdbcType=DECIMAL},
</if>
<if test="orderItem.commodityCount != null">
commodity_count = #{orderItem.commodityCount, jdbcType=INTEGER},
</if>
</set>
where item_id = #{orderItem.itemId, jdbcType=INTEGER}
</update>
如果上面 set 元素中的 if 元素判断均不为 null,则 set 元素中生成的 SQL 语句片段为:
set commodity_id = #{orderItem.commodityId, jdbcType=INTEGER},
commodity_price = #{orderItem.commodityPrice, jdbcType=DECIMAL},
commodity_count = #{orderItem.commodityCount, jdbcType=INTEGER}
生成的完成 SQL 语句如下:
update order_item
set commodity_id = #{orderItem.commodityId, jdbcType=INTEGER},
commodity_price = #{orderItem.commodityPrice, jdbcType=DECIMAL},
commodity_count = #{orderItem.commodityCount, jdbcType=INTEGER}
where item_id = #{orderItem.itemId, jdbcType=INTEGER}
WhereSqlNode
WhereSqlNode 的类型声明,成员变量和构造方法如下:
public class WhereSqlNode extends TrimSqlNode {
private static final 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);
}
}
WhereSqlNode 继承自 TrimSqlNode,用于处理动态 SQL 语句中的 where 元素。WhereSqlNode 指定成员变量 prefix 字段为 “where”,prefixesToOverride 字段为 "AND " 和 "OR ",suffix 字段和 suffixesToOverride 字段为 null。也就是说,使用 where 元素生成的 SQL 语句片段中,如果以“AND”或“OR”开头,MyBatis 会主动删除“AND”或“OR”,并在 SQL 语句片段的开头添加上 “where”。例如:
<select id="selectByItemIdAndOrderId" resultMap="BaseResultMap">
select * from order_item
<where>
<if test="orderItem.itemId != null">
and item_id = #{orderItem.itemId, jdbcType=INTEGER}
</if>
<if test="orderItem.orderId != null">
and order_id = #{orderItem.orderId, jdbcType=INTEGER}
</if>
</where>
</select>
如果上面 where 元素中的 if 元素判断均不为 null,则 where 元素中生成的 SQL 语句片段为:
where item_id = #{orderItem.itemId, jdbcType=INTEGER} and order_id = #{orderItem.orderId, jdbcType=INTEGER}
生成的完成 SQL 语句如下:
select * from order_item where item_id = #{orderItem.itemId, jdbcType=INTEGER} and order_id = #{orderItem.orderId, jdbcType=INTEGER}
从上面的内容也可以看到,set 元素和 where 元素只是内置了一些固定配置的 trim 元素,以此来实现在特定的 SQL 语句中简化 trim 元素配置的目的
ChooseSqlNode
ChooseSqlNode 的类型声明,成员变量和构造方法如下:
public class ChooseSqlNode implements SqlNode {
private final SqlNode defaultSqlNode;
private final List<SqlNode> ifSqlNodes;
public ChooseSqlNode(List<SqlNode> ifSqlNodes, SqlNode defaultSqlNode) {
this.ifSqlNodes = ifSqlNodes;
this.defaultSqlNode = defaultSqlNode;
}
}
ChooseSqlNode 用于处理动态 SQL 语句的 choose 元素,ChooseSqlNode 中声明了两个成员变量:
- SqlNode 类型的 defaultSqlNode,where 元素的子元素 otherwise 元素对应的 SqlNode;
- List<\SqlNode>类型的 ifSqlNodes,where 元素的子元素 when 元素对应的 SqlNode。
VarDeclSqlNode
VarDeclSqlNode 的类型声明,成员变量和构造方法如下:
public class VarDeclSqlNode implements SqlNode {
private final String name;
private final String expression;
public VarDeclSqlNode(String name, String exp) {
this.name = name;
this.expression = exp;
}
}
VarDeclSqlNode 用于处理动态 SQL 语句的 bind 元素,它声明了两个成员变量:
- String 类型的 name,用于声明变量,对应 bind 元素的 name 属性;
- String 类型的 expression,用于记录 OGNL 表达式,对应 bind 元素的 value 属性。
由于 choose 元素和 bind 元素在日常的工作中使用的场景较少,因此可能有部分小伙伴忘记了它俩的具体用法,忘记的小伙伴可以回顾下 《MyBatis映射器:实现动态SQL语句》。
构建 DynamicSqlSource
public class DynamicSqlSource implements SqlSource {
private final Configuration configuration;
private final SqlNode rootSqlNode;
public DynamicSqlSource(Configuration configuration, SqlNode rootSqlNode) {
this.configuration = configuration;
this.rootSqlNode = rootSqlNode;
}
}
这部分非常简单,结合前面的内容可以得知在调用 XMLScriptBuilder#parseScriptNode
方法创建 DynamicSqlSource 实例时,传入的 SqlNode 参数是由 XMLScriptBuilder#parseDynamicTags
方法解析 MyBatis 映射器中 SQL 语句生成的 MixedSqlNode 实例。也就是说,DynamicSqlSource 实例中并没有完整的 SQL 语句,只是由 SQL 语句的“碎片”(不同的 SqlNode 实例)和 MyBatis 核心配置文件在 MyBatis 应用程序中的映射 Configuration 实例组成。由于在 MyBatis 映射器文件解析的过程中,不会涉及到更多关于 SqlNode 和 SqlSoucre 的内容,因此这部分我们到这里就结束了。在后面涉及到 MyBatis 应用程序执行 SQL 语句的源码分析中我们还会再来聊 SqlNode 和 SqlSource 的。下一篇文章,我们就正式进入 MyBatis 应用程序执行 SQL 语句的源码分析了。
转载自:https://juejin.cn/post/7399494230677225507