13深入分析动态SQL语句解析全流程(下)
文章目录
在上一讲,我们讲解了 MyBatis 中动态 SQL 语句的相关内容,重点介绍了 MyBatis 使用到的 OGNL 表达式、组合模式、DynamicContext 上下文以及多个动态 SQL 标签对应的 SqlNode 实现。今天我们就紧接着上一讲,继续介绍剩余 SqlNode 实现以及 SqlSource 的相关内容。
SqlNode 剩余实现类
在上一讲我们已经介绍了 StaticTextSqlNode、MixedSqlNode、TextSqlNode、IfSqlNode、TrimSqlNode 这几个 SqlNode 的实现,下面我们再把剩下的三个 SqlNode 实现类也说明下。
1. ForeachSqlNode
在动态 SQL 语句中,我们可以使用
下面我们就来分析一下 ForeachSqlNode 的 apply() 方法是如何实现循环的。
首先,向 DynamicContext.sqlBuilder 中追加 open 属性值指定的字符串,然后通过 ExpressionEvaluator 工具类解析
接下来,为每个元素创建一个 PrefixedContext 对象。PrefixedContext 是 DynamicContext 的一个装饰器,其中记录了一个 prefix 前缀信息(其实就是
如果传入的集合是 Map 类型,则通过 applyIndex() 方法和 applyItem() 方法将 Map 中的 Key 和 Value 记录到 PrefixedContext 中,示例如下:
|
|
但如果传入的集合不是 Map 类型,则通过 applyIndex() 方法和 applyItem() 方法将集合元素的下标索引和元素值本身绑定到 PrefixedContext 中。
完成 PrefixedContext 的绑定之后,会调用
FilteredDynamicContext 变化过程示意图
下面是 FilteredDynamicContext.appendSql() 方法的核心实现:
|
|
完成集合中全部元素的迭代处理之后,ForeachSqlNode.apply() 方法还会调用 applyClose() 方法追加 close 属性指定的后缀。最后,从 DynamicContext 上下文中删除 index 属性值和 item 属性值指定的变量。
2. ChooseSqlNode
在有的业务场景中,可能会碰到非常多的分支判断,在 Java 中,我们可以通过 switch…case…default 的方式来编写这段代码;在 MyBatis 的动态 SQL 语句中,我们可以使用
IfSqlNode 和 MixedSqlNode 的核心实现在上一讲中我们已经分析过了,这里不再重复。ChooseSqlNode 的实现也比较简单,其中维护了一个 List
在 ChooseSqlNode 的 apply() 方法中,首先会尝试迭代全部 IfSqlNode 节点并执行 apply() 方法,我们知道任意一个 IfSqlNode.apply() 方法返回 true,即表示命中该分支,此时整个 ChooseSqlNode.apply() 返回 true,否则尝试执行 defaultSqlNode.apply() 方法并返回 true,即进入默认分支。如果 defaultSqlNode 字段为 null,则返回 false。
3. VarDeclSqlNode
VarDeclSqlNode 抽象了
VarDeclSqlNode 中的 name 字段维护了
在 apply() 方法中,VarDeclSqlNode 首先会通过 OGNL 工具类解析 expression 这个表达式的值,然后将解析结果与 name 字段的值一起绑定到 DynamicContext 上下文中,这样后面就可以通过 name 字段值获取这个表达式的值了。
SqlSourceBuilder
动态 SQL 语句经过上述 SqlNode 的解析之后,接着会由 SqlSourceBuilder 进行下一步处理。
SqlSourceBuilder 的核心操作主要有两个:
解析“#{}”占位符中携带的各种属性,例如,“#{id, javaType=int, jdbcType=NUMERIC, typeHandler=MyTypeHandler}”这个占位符,指定了 javaType、jdbcType、typeHandler 等配置;
将 SQL 语句中的“#{}”占位符替换成“?”占位符,替换之后的 SQL 语句就可以提交给数据库进行编译了。
SqlSourceBuilder 的入口是 parse() 方法,这里首先会创建一个识别“#{}”占位符的 GenericTokenParser 解析器,当识别到“#{}”占位符的时候,就由 ParameterMappingTokenHandler 这个 TokenHandler 实现完成上述两个核心步骤。
ParameterMappingTokenHandler 中维护了一个 List
在 buildParameterMapping() 方法中会通过 ParameterExpression 工具类解析“#{}”占位符,然后通过 ParameterMapping.Builder 创建对应的 ParameterMapping 对象。这里得到的 ParameterMapping 就会被记录到 parameterMappings 集合中。
ParameterMappingTokenHandler.handleToken() 方法的核心逻辑如下:
|
|
SqlSourceBuilder 完成了“#{}”占位符的解析和替换之后,会将最终的 SQL 语句以及得到的 ParameterMapping 集合封装成一个 StaticSqlSource 对象并返回。
SqlSource
经过上述一系列处理之后,SQL 语句最终会由 SqlSource 进行最后的处理。
在 SqlSource 接口中只定义了一个 getBoundSql() 方法,它控制着动态 SQL 语句解析的整个流程,它会根据从 Mapper.xml 映射文件(或注解)解析到的 SQL 语句以及执行 SQL 时传入的实参,返回一条可执行的 SQL。
下图展示了 SqlSource 接口的核心实现:
SqlSource 接口继承图
下面我们简单介绍一下这三个核心实现类的具体含义。
DynamicSqlSource:当 SQL 语句中包含动态 SQL 的时候,会使用 DynamicSqlSource 对象。
RawSqlSource:当 SQL 语句中只包含静态 SQL 的时候,会使用 RawSqlSource 对象。
StaticSqlSource:DynamicSqlSource 和 RawSqlSource 经过一系列解析之后,会得到最终可提交到数据库的 SQL 语句,这个时候就可以通过 StaticSqlSource 进行封装了。
1. DynamicSqlSource
DynamicSqlSource 作为最常用的 SqlSource 实现,主要负责解析动态 SQL 语句。
DynamicSqlSource 中维护了一个 SqlNode 类型的字段(rootSqlNode 字段),用于记录整个 SqlNode 树形结构的根节点。在 DynamicSqlSource 的 getBoundSql() 方法实现中,会使用前面介绍的 SqlNode、SqlSourceBuilder 等组件,完成动态 SQL 语句以及“#{}”占位符的解析,具体的实现如下:
|
|
这里最终返回的 BoundSql 对象,包含了解析之后的 SQL 语句(sql 字段)、每个“#{}”占位符的属性信息(parameterMappings 字段 ,List
后面在讲解 StatementHandler、Executor 如何执行 SQL 语句的时候,我们还会继续介绍 BoundSql 的相关内容,到时候你可以跟这里联系起来学习。
2. RawSqlSource
接下来我们看 SqlSource 的第二个实现—— RawSqlSource,它与 DynamicSqlSource 有两个不同之处:
RawSqlSource 处理的是非动态 SQL 语句,DynamicSqlSource 处理的是动态 SQL 语句;
RawSqlSource 解析 SQL 语句的时机是在初始化流程中,而 DynamicSqlSource 解析动态 SQL 的时机是在程序运行过程中,也就是运行时解析。
这里我们需要先来回顾一下前面介绍的 XMLScriptBuilder.parseDynamicTags() 方法,其中会判断一个 SQL 片段是否为动态 SQL,判断的标准是:如果这个 SQL 片段包含了未解析的“${}”占位符或动态 SQL 标签,则为动态 SQL 语句。但注意,如果是只包含了“#{}”占位符,也不是动态 SQL。
XMLScriptBuilder. parseScriptNode() 方法而 会判断整个 SQL 语句是否为动态 SQL,判断的依据是:如果 SQL 语句中包含任意一个动态 SQL 片段,那么整个 SQL 即为动态 SQL 语句。
总结来说,对于动态 SQL 语句,MyBatis 会创建 DynamicSqlSource 对象进行处理,而对于非动态 SQL 语句,则会创建 RawSqlSource 对象进行处理。
RawSqlSource 在构造方法中,会调用 SqlNode.apply() 方法将 SQL 片段组装成完整 SQL,然后通过 SqlSourceBuilder 处理“#{}”占位符,得到 StaticSqlSource 对象。这两步处理与 DynamicSqlSource 完全一样,只不过执行的时机是在 RawSqlSource 对象的初始化过程中(即 MyBatis 框架初始化流程中),而不是在 getBoundSql() 方法被调用时(即运行时)。
最后,RawSqlSource.getBoundSql() 方法实现是直接调用 StaticSqlSource.getBoundSql() 方法返回一个 BoundSql 对象。
通过前面的介绍我们知道,无论是 DynamicSqlSource 还是 RawSqlSource,底层都依赖 SqlSourceBuilder 解析之后得到的 StaticSqlSource 对象。StaticSqlSource 中维护了解析之后的 SQL 语句以及“#{}”占位符的属性信息(List
总结
我们紧接上一讲的内容,往后介绍了 SqlNode 接口剩余的实现类,其中包括 ForeachSqlNode、ChooseSqlNode 等,这些 SqlNode 实现类都对应我们常用的动态 SQL 标签。
接下来,我们还介绍了 SqlSourceBuilder 以及 SqlSource 接口的内容,其中针对不同类型的 SQL 语句,MyBatis 抽象出了不同的 SqlSource 实现类,也就是文中介绍的 DynamicSqlSource、RawSqlSource 以及 StaticSqlSource。
在前面的第 11 讲中,我们介绍了 MyBatis 对
《Java 工程师高薪训练营》
实战训练+面试模拟+大厂内推,想要提升技术能力,进大厂拿高薪,点击链接,提升自己!
-– ### 精选评论
文章作者
上次更新 10100-01-10