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