前面文章介绍了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语句的过程。