Mybatis之动态Sql解析

前面文章介绍了Mybatis中Sql运行的全过程,本篇文章来分析动态Sql是如何解析的。

首先动态Sql是指事先无法预知具体的条件,需要在运行时根据具体的情况动态地生成SQL语句。也就是我们常见的if、where、foreach等带有这些标签的Sql。

SqlSource与BoundSql

在了解动态Sql的原理前需要先了解几个动态Sql的相关组件,即SqlSource和BoundSql。 SqlSource用来描述sql资源,SqlSource接口只有一个方法getBoundSql(),该方法返回一个BoundSql实例。BoundSql是对SQL语句及参数信息的封装,它是SqlSource解析后的结果。

SqlSource有4个实现类,其结构如下:

其中ProviderSqlSource用于描述通过注解配置的SQL信息,如@Select等注解

DynamicSqlSource用于描述动态SQL的信息以及带${}参数占位符的Sql信息,这些都是在具体调用时才能确定具体的SQL语句。

RawSqlSource用于描述静态Sql语句信息,在解析xml的时候即可确定具体的SQL信息。

StaticSqlSource用于描述ProviderSqlSource、DynamicSqlSource及RawSqlSource解析后得到的静态SQL资源。

也就是说最终所有的Sql信息都会用StaticSqlSource来描述。我们可以看看staticSqlSource的相关逻辑。

如图所示,StaticSqlSource只封装了Mapper解析后的SQL内容和Mapper参数映射信息。

BoundSql的封装如下所示:

相比较SqlSource而言,BoundSql除了封装了Mapper解析后的SQL语句和参数映射信息外,还封装了Mapper调用时传入的参数对象。

LanguageDriver

Mybatis通过SqlSource描述具体的Sql资源,通过LanguageDriver将xml或者注解的Sql信息转变为SqlSouce。 如图所示:

LanguageDriver接口中一共有3个方法,其中createParameterHandler()方法用于创建ParameterHandler对象,另外还有两个重载的createSqlSource()方法,这两个重载的方法用于创建SqlSource对象。

Myvbatis提供两个实现类分别是XMLLanguageDriver和RawLanguageDriver,XMLLanguageDriver实现动态SQL的功能。RawLanguageDriver表示仅支持静态SQL配置。

如上图所示,XMLLanguageDriver的两个重载方法分别表示从XML中获取SQL配置和从注解中获取SQL对象。

第一个方法处理XML的Sql,该方法中创建了一个XMLScriptBuilder对象,然后调用XMLScriptBuilder对象的parseScriptNode()方法将SQL资源转换为SqlSource对象。

第二个重载的createSqlSource()方法用于处理Java注解中配置的SQL信息,该方法中首先判断SQL配置是否以script标签开头,如果是,则以XML方式处理Java注解中配置的SQL信息,否则简单处理,替换SQL中的全局参数。如果SQL中仍然包含${}参数占位符,则SQL语句仍然需要根据传递的参数动态生成,所以使用DynamicSqlSource对象描述SQL资源,否则说明SQL语句不需要根据参数动态生成,使用RawSqlSource对象描述SQL资源。

从第二个方法可以看出,注解方式也支持动态Sql的功能,只需要Sql以script标签开头即可正常解析。

SqlNode

SqlNode接口用于解析Sql节点,根据对应的参数信息生成静态SQL的内容,如下所示,SqlNode的实现类非常多。

使用的动态Sql的每一个标签在SqlNode中都能找到对应的实现类,相应的类解析相应的SQL片段。其中几个特殊的SqlNode需要介绍一下,如MixedSqlNode,StaticTextSqlNode以及TextSqlNode。

其中MixedSqlNode用于描述一组SqlNode对象,通常一个Mapper配置是由多个SqlNode对象组成的,这些SqlNode对象通过MixedSqlNode进行关联,组成一个完整的动态SQL配置。

staticSqlNode用于描述动态SQL中的静态文本内容。

TextSqlNode与StaticTextSqlNode类不同的是,当静态文本中包含 <math xmlns="http://www.w3.org/1998/Math/MathML"> 占位符时,说明 {}占位符时,说明 </math>占位符时,说明{}需要在Mapper调用时将${}替换为具体的参数值。因此,使用TextSqlNode类来描述。

一个简单的例子说明SqlNode之间如何配合生成一个Sql。如下图所示,是一个比较常见的动态Sql的实例:

其使用到的SqlNode如下图所示。

在上面的代码中,我们创建了一个StaticTextSqlNode和三个IfSqlNode来描述Mapper中动态SQL的配置,其中IfSqlNode由一个StaticTextSqlNode和条件表达式组成。接着创建了一个MixedSqlNode将这些SqlNode组合起来,这样就完成了通过Java对象来描述动态SQL配置。SqlNode对象创建完毕后,我们就可以调用MixedSqlNode的apply()方法根据参数内容动态地生成SQL内容了。该方法接收一个DynamicContext对象作为参数,DynamicContext对象中封装了Mapper调用时的参数信息。上面的代码中,我们创建了一个DynamicContext,然后调用MixedSqlNode对象的apply()方法,动态SQL的解析结果封装在DynamicContext对象中,我们只需要调用DynamicContext对象的getSql()方法即可获取动态SQL解析后的SQL语句。运行上面这段代码后,生成的SQL内容如下: 各个动态标签解析的SqlNode实现都大差不差,基本都是解析表达式,之后验证表达式值为真,拼接Sql。 其中比较特殊的MixedSqlNode实际上是聚合了这些动态标签对应的SqlNode节点,之后循环所有SqlNode的apply()方法,如下所示:

staticTextSqlNode对象更为简单,只需要将Sql文本内容追加到DynamicContext对象中。

动态Sql的解析过程

回到BaseExecutor执行Query()的逻辑,可以看到在一开始就先获取了BoundSql对象 BoundSql是由MappedStatement对象的getBoundSq()方法获取,我们继续看看getBoundSql方法:

可以看到BoundSql从SqlSource中获取。那么SqlSource是如何获取的呢,前面代码可以看到SqlSource通过LanguageDriver的createSqlSource创建,其最终调用XMLScriptBuilder的parseScriptNode()。

如上面的代码所示,在XMLScriptBuilder类的parseScriptNode()方法中,调用parseDynamicTags()方法将SQL配置转换为SqlNode对象,然后判断SQL配置是否为动态SQL,如果为动态SQL,则创建DynamicSqlSource对象,否则创建RawSqlSource对象。

接下来看看parseDynamicrTags()方法,如下所示:

XMLScriptBuilder类的parseDynamicTags()方法的逻辑相当复杂,在该方法中对SQL配置的所有子元素进行遍历,如果子元素类型为SQL文本,则使用TextSqlNode对象描述SQL节点信息,若SQL节点中存在${}参数占位符,则设置XMLScriptBuilder对象的isDynamic属性值为true;如果子元素有动态SQL的如if、where等标签,则使用对应的NodeHandler处理。XMLScriptBuilder类中定义了一个私有的NodeHandler接口,并为每种动态SQL标签提供了一个NodeHandler接口的实现类,通过实现类处理对应的动态SQL标签,把动态SQL标签转换为对应的SqlNode对象。NodeHandler的实现类如下所示:

其作用便是将各种动态SQL片段转换为对应的SqlNode,并将其添加到contents中。

如上我们可以看到最终获取SqlBound的全过程,简单回顾下,从SqlSource中获取BoundSql,SqlSource由XMLScriptBuilder的parseScriptNode()方法获取,在获取SqlSource的时候通过parseDynamicrTags()方法构造SqlNode对象。

动态Sql解析完成之后的结果如下所示:

如上Sql解析完成之后还需要将其中的动态字符替换掉。前面介绍过动态sql最终会用DynamicSqlSource类来描述,我们看看DynamicSqlSource的getBoundSql()方法。

其先构造一个SqlSourceBuilder类,通过SqlSourceBuilder的Parse()方法最终获取SqlSource对象。我们上面同样也提及过最终所有的SqlSource都通过StaticSqlSource描述,可以从SqlSourceBuilder的Parse()方法看到。如下所示:

同样在上面代码中我们也可以观察到有一个GenericTokenParser比较显眼,因为在其中我们看到了#标识符,可以猜到一定是在这里对Sql进行了最终的替换,那么替换成什么了尼? 我们可以看看GenericTokenParser类

有一个TokenHanlder类需要引起重视:

可以看到最终的实现方法handleToken(),其返回一个?号,这里也就比较熟悉了,经常使用Mybatis的应该或多或少见过Mybtais的报错,其报错Sql动态替换的值都是?。其最终?就是在这里替换的。

为什么要替换成一个"?"字符呢,大家应该会联想到JDBC中的PreparedStatement,MyBatis默认情况下会使用PreparedStatement对象与数据库进行交互,因此#{}参数占位符内容被替换成了问号,然后调用PreparedStatement对象的setXXX()方法为参数占位符设置值。

至此动态Sql解析完毕。

总结

SqlSource用于描述MyBatis中的SQL资源信息,LanguageDriver用于解析SQL配置,将SQL配置信息转换为SqlSource对象,SqlNode用于描述动态SQL中、等标签信息,LanguageDriver解析SQL配置时,会把、等动态SQL标签转换为SqlNode对象,封装在SqlSource中。而解析后的SqlSource对象会作为MappedStatement对象的属性保存在MappedStatement对象中。执行Mapper时,会根据传入的参数信息调用SqlSource对象的getBoundSql()方法获取BoundSql对象,这个过程就完成了将SqlNode对象转换为SQL语句的过程。

相关推荐
郑祎亦1 小时前
Spring Boot 项目 myblog 整理
spring boot·后端·java-ee·maven·mybatis
jokerest12315 小时前
web——sqliabs靶场——第十三关——报错注入+布尔盲注
mybatis
武子康15 小时前
Java-06 深入浅出 MyBatis - 一对一模型 SqlMapConfig 与 Mapper 详细讲解测试
java·开发语言·数据仓库·sql·mybatis·springboot·springcloud
WindFutrue18 小时前
使用Mybatis向Mysql中的插入Point类型的数据全方位解析
数据库·mysql·mybatis
AiFlutter19 小时前
Java实现简单的搜索引擎
java·搜索引擎·mybatis
天天扭码1 天前
五天SpringCloud计划——DAY1之mybatis-plus的使用
java·spring cloud·mybatis
武子康2 天前
Java-05 深入浅出 MyBatis - 配置深入 动态 SQL 参数、循环、片段
java·sql·设计模式·架构·mybatis·代理模式
2的n次方_2 天前
MyBatis——#{} 和 ${} 的区别和动态 SQL
数据库·sql·mybatis
jokerest1232 天前
web——sqliabs靶场——第十二关——(基于错误的双引号 POST 型字符型变形的注入)
数据库·sql·mybatis