SqlSource
SqlSource
是mybatis中的一个接口,我们平时的mapper文件中的sql解析后的结果便存放在SqlSource
中,在mybatis后续的sql操作流程中,都会间接的获取到sql方法对应的SqlSource
对象,再进一步处理得到此次sql操作的sql语句,入参等信息。
SqlSource实现类简介
1.RawSqlSource
用于对不存在动态标签的sql进行处理。
2.DynamicSqlSource
用于对存在动态标签的sql进行处理。
3.StaticSqlSource
存储最终的sql模板。RawSqlSource
跟DynamicSqlSource
会先一步步的解析过程,如动态标签判断,非参数替换等。完成后都会创建StaticSqlSource
对象,用于后续sql的实际执行,避免重复解析。
4.ProviderSqlSource
此实现类略,以下流程也不进行说明。
SqlSource的创建流程
一个mappper xml文件下的sql标签,对应一个SqlSource
。我们看看SqlSource
是如何创建的
1.RawSqlSource、DynamicSqlSource的创建
定位到关键代码XMLStatementBuilder#parseStatementNode
1.SqlSource
是通过LanguageDriver#createSqlSource
创建的
2.创建后的SqlSource
会存入MappedStatement
中,用于后续查询使用。
继续看LanguageDriver#createSqlSource
后的方法,定位到关键方法XMLScriptBuilder#parseScriptNode
,这里解释了不同SqlSource
类型对象的创建逻辑。
如果SqlSource是动态的,就生动动态类型的SqlSource,否则生成真实类型的SqlSource
关键问题来了,什么样的sql标签才算是动态的呢? 看下isDynamic
值逻辑的关键方法XMLScriptBuilder#parseDynamicTags
- 如果sql xml下的节点类型为CDATA_SECTION_NODE 或者TEXT_NODE ,且节点
isDynamic
,则说明当前sql是动态的,即isDynamic
为true
- 如果sql xml下的节点类型为ELEMENT_NODE ,会获取对应元素的解析器进行处理,且当前sql是动态的,即
isDynamic
为true
以上两种情况,代表sql标签是动态的。
下面稍微解释下CDATA_SECTION_NODE ,TEXT_NODE ,ELEMENT_NODE 这几个节点类型
1. CDATA_SECTION_NODE
用于存储不需要转义的文本。
该示例中, <![CDATA[ select * from user where userName like #{userName} ]]>
,为一个CDATA 类型的节点,值为select * from user where userName like #{userName}
2. ELEMENT_NODE
代表节点为一个元素。
该示例中<where> user_name = ${userName} </where>
为一个ELEMENT_NODE 节点,值为 user_name = ${userName}
这种类型的节点,mybatis会根据节点名称,如where,if,foreach
等获取对应的节点解析器进行sql解析。
3. TEXT_NODE
代表节点为一个文本值。
跟上面同样的示例,select * from user
是一个TEXT_NODE 节点,值为select * from user
以上便是相关XML节点类型的说明,回到isDynamic
值的判断条件,其中一条是
如果sql xml下的节点类型为CDATA_SECTION_NODE 或者TEXT_NODE ,且节点为isDynamic
,则说明当前sql是动态的,即isDynamic
为true
现在已经知道什么节点类型才为CDATA_SECTION_NODE 或者TEXT_NODE 了,接着说明什么类型的节点为isDynamic
。
核心方法为TextSqlNode#isDynamic
。创建了一个文本解析器,解析文本节点中是否存在${开头,}结尾的文本块,如果存在,节点则是动态的,值isDynamic
为true
以上便是sql语句对应SqlSource类型的代码逻辑
总结:
1. sql语句标签中,存在 ${xxx} 格式的文本块,则创建的SqlSource
类型为DynamicSqlSource
2. sql语句标签中,存在子级元素,如 <where> <if> <foreach>
等,则创建的·SqlSource
类型为 DynamicSqlSource
3. 其他情况,创建的SqlSource
类型为RawSqlSource
**
2.StaticSqlSource
以上便为 DynamicSqlSource
,RawSqlSource
类型对象的创建过程,接下来继续说StaticSqlSource
类型对象的创建过程。
StaticSqlSource
对象是由 DynamicSqlSource
,RawSqlSource
类型对象进一步解析而来的,是mybatis查询过程中最终使用到的类型。DynamicSqlSource
,RawSqlSource
类型的对象只是一个中间状态。
关键方法为SqlSourceBuilder#parse
,整个项目中只有这里创建了StaticSqlSource
对象,而该方法也只有DynamicSqlSource
,RawSqlSource
进行了调用。
可以得出初步结论: StaticSqlSource
对象是由 DynamicSqlSource
,RawSqlSource
类型对象进一步解析而来的
分析下SqlSourceBuilder#parse
做了什么事情
-
创建一个针对#{xxx}格式字符串的解析器
GenericTokenParser
-
调用
GenericTokenParser#parse
,对DynamicSqlSource
、RawSqlSource
中的sql进行进一步解析,如果带有#{xxx}格式的字符串,则替换为'?'这里便是#{xxx}格式的参数会被预编译的原因。
说下是怎么将#{xxx}格式字符串替换为'?'的,debug进GenericTokenParser#parse
,如果匹配到#{xxx}格式的字符串,会将sql中#{xxx}格式字符串的位置,添加为TokenHandler#handleToken
的返回值,这里看到是一个'?'
此次解析的sql
进去TokenHandler#handleToken
看看,这里会将#{userName}的userName放入存放参数key的list parameterMappings
中,然后返回'?'
StaticSqlSource被调用的地方
以上便是SqlSourceBuilder#parse
方法的解析,继续看DynamicSqlSource
,RawSqlSource
调用该方法的地方
RawSqlSource
生成了一个StaticSqlSource
作为成员变量,在getBoundSql
方法中被实际用到,调用RawSqlSource#getBoundSql
时会调用StaticSqlSource#getBoundSql
DynamicSqlSource
每次被调用getBoundSql
方法时,都会创建一个StaticSqlSource
对象,调用StaticSqlSource#getBoundSql
总结:
1. StaticSqlSource
对象是由 DynamicSqlSource
,RawSqlSource
对象进一步解析而来的。如果DynamicSqlSource
,RawSqlSource
的sql中存在#{xxx}字符串,会被替换为'?',这个是#{xxx}参数会被预编译的原因。
2. StaticSqlSource
是sql标签解析后的结果,在sql查询时会被mybatis调用到getBoundSql
方法。
BoundSql
mybatis执行sql操作时,每次都会创建一个BoundSql
对象,获取sql内容,入参,映射等信息的。
1.BoundSql的创建流程
通过上文可知, BoundSql
是调用 DynamicSqlSource
,RawSqlSource
的getBoundSql
生成的。这两个类又是调用StaticSqlSource#getBoundSql
生成的BoundSql
对象,再进行进一步加工。
RawSqlSource
RawSqlSource#getBoundSql
没有其他操作,单纯调用StaticSqlSource#getBoundSql
返回值
DynamicSqlSource
流程如下:
1. 根据入参情况拼接最终的sql。因为是动态类型的sql,所以存在<where> <if> <foreach>
等标签,需要在查询的时候才能进行sql解析拼接。
2. 拼接的如果是字符串,不是标签节点,如果字符串存在${xxx},会替换为入参xxx字段对应的值。
3. 拼接完的sql,如果存在#{xxx},会将入参xxx的值存起来,并将sql此处替换为'?'。
2. 为什么需要使用BoundSql而不直接用SqlSource
我们需要先知道,BoundSql
存在的意义,如果要获取sql的话直接通过SqlSource
获取不就行吗,何必多加个类呢?以下原因为个人总结,可能存在歧义,参考即可。
因为SqlSource
只存储了sql本身的信息,并不存储每次查询实际的参数,SqlSource
可以理解为一个中间状态的sql模板。
在sql解析的时候。获取到sql模板后,还要做最后一步操作:入参拼接,才能生成最终执行的sql,存进BoundSql
中
3.BoundSql在sql操作时被使用的地方
PreparedStatementHandler#instantiateStatement
,这是mybatis创建Statement
对象,再去jdbc执行操作的方法,调用boundSql.getSql()
获取到了最终被执行的sql。
DefaultParameterHandler#setParameters
,调用boundSql.getParameterMappings()
获取了此次sql的参数,再调用typeHandler#setParameter
将参数值放进Statement
对象中,接着调用jdbc相关接口,完成此次sql操作。