Mybatis源码之美:3.6.解析sql代码块
解析sql代码块
在处理了复杂繁琐的resultMap
元素的解析过程之后,这篇文章我们来学习一个比较简单的元素--sql
元素.
在mybatis
中,我们可以使用sql
元素定义部分SQL
语句,以达到代码复用的效果.
我们可以通过include
标签来引用已配置的sql
元素.
关于
include
元素的解析操作,我们会在后面的文章中给出,现在我们只需要了解include
标签拥有一个指向被引用sql
元素的refid
属性定义.
比如,下面的配置:
<sql id="allColumns">
id,name
</sql>
<select id="selectUserByIdWithIncude" resultType="org.apache.learning.sql.User">
SELECT
<include refid="allColumns"/>
FROM USER u
WHERE u.id=#{id}
</select>
效果等同于:
<select id="selectUserById" resultType="org.apache.learning.sql.User">
SELECT
id,name
FROM USER u
WHERE u.id=#{id}
</select>
甚至于,我们还可以在sql
代码块中包含动态代码参数:
<sql id="whereId">
u.id=#{id}
</sql>
<select id="selectUserById" resultType="org.apache.learning.sql.User">
SELECT
id,name
FROM USER u
WHERE
<include refid="whereId"/>
</select>
当然上面的
WHERE <include refid="whereId"/>
可以通过动态sql标签where
来实现:<where> u.id=#{id} </where>
sql
元素的定义并不复杂,他有三个属性定义:
<!ATTLIST sql
id CDATA #REQUIRED
lang CDATA #IMPLIED
databaseId CDATA #IMPLIED
>
其中必填的id
属性是sql
元素的唯一标志,lang
表示该sql
元素对应的脚本语言,databaseId
表示sql
语句对应的数据库类型.
从
3.2
版本开始,mybatis
开始支持脚本语言,允许我们通过指定的语言驱动来加载SQL
语句.
上面说的是sql
元素的属性定义,除此之外,sql
元素还有一些子元素定义:
<!ELEMENT sql (#PCDATA | include | trim | where | set | foreach | choose | if | bind)*>
这些子元素中除了include
元素之外,都用于配置动态sql
,关于动态sql
的内容我们会在后面的文章中给出.
如果仔细观察sql
元素的DTD
定义,我们会发现和前面学习的元素有所不同的是sql
元素多了一个#PCDATA
的类型标记.
如果要理解PCDATA
标记的含义,那么我们就需要简单了解一些关于XML
解析器的术语.
首先我们要知道,在XML
中有五个拥有特殊含义的字符,他们分别是>
,<
,&
,'
以及"
.
这五个特殊字符无法直接使用,当我们需要使用这五个特殊字符时,有两种解决方案,一种是使用对应的替代字符:
特殊字符 | 替代字符 | 原意 |
---|---|---|
< |
< |
less than |
> |
> |
greater than |
& |
&t; |
ampersand |
' |
' |
apostrophe |
" |
" |
straight double quotation mark |
另一种是通过语法<![CDATA[字符]]>
来标记我们使用的特殊字符,比如:使用<![CDATA[<]]>
来表示<
.
这里提到的CDATA
就是一个XML
解析器的术语,它是Character Data
的缩写,表示不应被XML
解析器解析的文本数据,他还有一个名字叫做Unparsed Character Data
,因此CDATA
对应的文本中的标签会被当做普通文本,不会被解析.
与之相对应的就是术语PCDATA
,PCDATA
是Parsed Character Data
的缩写,表示应该由XML
解析器解析的文本数据,PCDATA
对应的文本中的标签会被正常解析.
所以,根据sql
元素上的PDATA
标记,我们可以大概断定sql
元素的性质:sql
元素中的文本定义,允许子元素和普通文本混排.
在了解了sql
元素的基本信息之后,我们正式看一下sql
元素的解析操作,sql
元素的解析入口在XMLMapperBuilder
的configurationElement()
方法中:
private void configurationElement(XNode context){
// ... 省略 ...
// 解析并注册Sql元素,此处只是简单的将所有的SQL片段读取出来,然后放到{@link #sqlFragments}中,
// 不会执行太多额外的操作
sqlElement(context.evalNodes("/mapper/sql"));
// ... 省略 ...
}
configurationElement()
调用sqlElement()
方法来完成元素的解析工作:
/**
* 解析并注册 所有的Sql元素
* 会解析所有没有指定数据库标志的SQL片段以及当前数据库类型的SQL片段
* 此处只是简单的将所有的SQL片段读取出来,然后放到{@link #sqlFragments}中。
*
* @param list 所有的/mapper/sql节点
*/
private void sqlElement(List<XNode> list) {
if (configuration.getDatabaseId() != null) {
// 获取当前数据库类型的专用SQL片段
sqlElement(list, configuration.getDatabaseId());
}
// 获取所有没有指定数据库类型的SQL片段
sqlElement(list, null);
}
看上面的代码实现,我们可以发现mybaits
默认会加载所有未限制数据库类型的sql
元素,以及能够匹配当前数据库类型的sql
元素.
千万不要小瞧这一个小特性,他是mybatis
实现的跨数据库语句支持的基础.
重载的sqlElement()
方法的实现非常简单:
/**
* 解析并注册Sql节点代码块
*
* @param list 所有的SQL节点
* @param requiredDatabaseId 当前的数据库类型标志
*/
private void sqlElement(List<XNode> list, String requiredDatabaseId) {
for (XNode context : list) {
// 获取数据库类型标志
String databaseId = context.getStringAttribute("databaseId");
// 获取Sql代码块的唯一标志
String id = context.getStringAttribute("id");
// 将唯一标志和当前命名空间结合
id = builderAssistant.applyCurrentNamespace(id, false);
if (databaseIdMatchesCurrent(id, databaseId, requiredDatabaseId)) {
// 当前Sql代码块属于当前数据库类型,保留当前代码块
sqlFragments.put(id, context);
}
}
}
针对每一个sql
元素,mybatis
都会通过MapperBuilderAssistant
的applyCurrentNamespace()
方法将其id
转换为全局唯一的标志.
然后将通过databaseIdMatchesCurrent()
方法校验的sql
元素,存放到XMLMapperBuilder
的sqlFragments
集合中,供后续的解析过程使用.
负责校验sql
元素有效性的databaseIdMatchesCurrent()
方法的处理逻辑也非常简单:
private boolean databaseIdMatchesCurrent(String id, String databaseId, String requiredDatabaseId) {
if (requiredDatabaseId != null) {
if (!requiredDatabaseId.equals(databaseId)) {
return false;
}
} else {
if (databaseId != null) {
return false;
}
// skip this fragment if there is a previous one with a not null databaseId
if (this.sqlFragments.containsKey(id)) {
XNode context = this.sqlFragments.get(id);
if (context.getStringAttribute("databaseId") != null) {
return false;
}
}
}
return true;
}
如果当前sql
元素指定了databaseId
属性,那么就和调用sqlElement()
方法时传入的requiredDatabaseId
属性相比较,当前sql
元素是否有效,取决于两个属性的取值是否一致.
如果当前sql
元素没有指定databaseId
属性,在当前尚未有相同id
的sql
元素注册进来的前提下,那么该元素就是有效的.
值得注意的是,前面的sqlElement()
方法调用了两次重载的sqlElement()
方法,第一次调用时,指定了requiredDatabaseId
参数,第二次没有指定.
因此,结合着databaseIdMatchesCurrent()
方法的实现来看,针对具有相同id
属性的sql
元素,如果同时匹配了指定databaseId
和未指定databaseId
属性的两个sql
元素,未指定databaseId
属性的sql
元素将会被忽略.
这就是关于sql元素的解析过程了,相对来说比较简单,我本打算将动态sql
相关的内容放到这篇文章中,后来仔细想了想,还是放到后面来说吧.
就酱,告辞!