解析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相关的内容放到这篇文章中,后来仔细想了想,还是放到后面来说吧.
就酱,告辞!