今天我们接着来学习 MyBatis 应用程序初始化过程中的源码,前面 我们已经分析了 XMLScriptBuilder#parseDynamicTags
方法中处理静态 SQL 语句的部分,今天我们来看处理动态 SQL 语句的部分。
XMLScriptBuilder#parseDynamicTags 方法分析
上一篇文章 中,我们提到 XMLScriptBuilder#parseDynamicTags
方法用于生成 MixedSqlNode 实例,不过我们只看了静态 SQL 语句生成的部分,今天我们来看用于生成动态 SQL 语句的 MixedSqlNode 实例的部分,源码如下:
java
代码解读
复制代码
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 中的元素的处理:
- 第一次进入循环时,处理的是
select * from order_item
语句,这里会按照静态 SQL 语句来处理,生成 StaticTextSqlNode 实例并添加到 contents 中,这点我们在 《MyBatis映射器文件解析:sql元素与静态SQL语句》 中已经聊过了; - 跌二次进入循环时,处理的是 where 元素,这里是按照动态 SQL 语句来处理的,处理方式会复杂有一些,我们先从整体上来理解第 10 行代码到第 13 行代码的内容。
- 第 10 行代码,获取元素的名称,这里就是 where;
- 第 11 行代码,通过元素名称获取对应的 NodeHandler 实例,用于存储 NodeHandler 实例的容器 nodeHandlerMap 是在构造 XMLScriptBuilder 时调用
XMLScriptBuilder#initNodeHandlerMap
方法完成的初始化; - 第 12 行代码,调用
NodeHandler#handler
方法,处理动态 SQL 语句,大部分 NodeHandler 的实现类实现的NodeHandler#handler
方法中会递归调用XMLScriptBuilder#parseDynamicTags
方法,用于处理当前元素的子元素; - 第 13 行代码,将该 SQL 语句标记为动态 SQL 语句。
下面我们着重分析第 11 行代码和第 12 行代码中出现的对象和方法进行分析。
XMLScriptBuilder#initNodeHandlerMap 方法
上一篇文章 在介绍 XMLScriptBuilder 的构造方法时可以看到构造方法中调用了 XMLScriptBuilder#initNodeHandlerMap
方法,并且我添加了注释“初始��用于处理动态 SQL 语句的 Map”说明了该方法真的作用,下面我们来看该方法的源码,如下:
整理了这份Java面试笔记包括了:Java面试、Spring、JVM、MyBatis、Redis、MySQL、并发编程、微服务、Linux、Springboot、SpringCloud、MQ、Kafka 面试专题
需要全套面试笔记【点击此处】即可免费获取
java
代码解读
复制代码
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 接口中只定义了一个方法:
java
代码解读
复制代码
private interface NodeHandler { void handleNode(XNode nodeToHandle, List<SqlNode> targetContents); }
而 NodeHandler 的实现类在实现 NodeHandler#handleNode
方法时,没有进行特别的处理,只是解析对应元素中的属性,并生成相应的 SqlNode,这里我以最常见的 IfHandler 为例进行说明,源码如下:
java
代码解读
复制代码
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 中定义的接口,它只定义了一个方法:
java
代码解读
复制代码
public interface SqlNode { boolean apply(DynamicContext context); }
该方法会根据传入参数,解析 SqlNode 记录的动态 SQL 语句,这个方法我们会在 MyBatis 应用程序执行 SQL 语句的内容中再深入的了解。
SqlNode 有 10 个实现类,如下:
下面我们就来了解每个 SqlNode 的实现类的类型声明和构造方法。
MixedSqlNode
MixedSqlNode 的类型声明,成员变量和构造方法如下:
java
代码解读
复制代码
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 的类型声明,成员变量和构造方法如下:
java
代码解读
复制代码
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 的类型声明,成员变量和构造方法如下:
java
代码解读
复制代码
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 的类型声明,成员变量和构造方法如下:
java
代码解读
复制代码
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 的类型声明,成员变量和构造方法如下:
java
代码解读
复制代码
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 的类型声明,成员变量和构造方法如下:
java
代码解读
复制代码
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 的类型声明,成员变量和构造方法如下:
java
代码解读
复制代码
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”。例如:
xml
代码解读
复制代码
<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 语句片段为:
sql
代码解读
复制代码
set commodity_id = #{orderItem.commodityId, jdbcType=INTEGER}, commodity_price = #{orderItem.commodityPrice, jdbcType=DECIMAL}, commodity_count = #{orderItem.commodityCount, jdbcType=INTEGER}
生成的完成 SQL 语句如下:
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 的类型声明,成员变量和构造方法如下:
java
代码解读
复制代码
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”。例如:
xml
代码解读
复制代码
<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 语句片段为:
sql
代码解读
复制代码
where item_id = #{orderItem.itemId, jdbcType=INTEGER} and order_id = #{orderItem.orderId, jdbcType=INTEGER}
生成的完成 SQL 语句如下:
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 的类型声明,成员变量和构造方法如下:
java
代码解读
复制代码
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 的类型声明,成员变量和构造方法如下:
java
代码解读
复制代码
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
上面我们已经把 SqlNode 和它的实现类的基础信息介绍完了,并且在 《MyBatis映射器文件解析:sql元素与静态SQL语句》 中,我们也简单介绍过 SqlSource 和它的实现类 RawSqlSource,下面我们就来看下 SqlSource 中动态 SQL 语句所使用的 DynamicSqlSource。
DynamicSqlSource 的类型声明,成员变量和构造方法如下:
java
代码解读
复制代码
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 的。