MyBatis映射器文件解析:动态SQL语句

今天我们接着来学习 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 的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值