转载

Mybatis源码之美:3.6.解析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 中有五个拥有特殊含义的字符,他们分别是 > , < , & , ' 以及 " .

这五个特殊字符无法直接使用,当我们需要使用这五个特殊字符时,有两种解决方案,一种是使用对应的替代字符:

特殊字符 替代字符 原意
< &lt; less than
> &gt; greater than
& &ampt; ampersand
' &apos; apostrophe
" &quot; straight double quotation mark

另一种是通过语法 <![CDATA[字符]]> 来标记我们使用的特殊字符,比如:使用 <![CDATA[<]]> 来表示 < .

这里提到的 CDATA 就是一个 XML 解析器的术语,它是 Character Data 的缩写,表示不应被 XML 解析器解析的文本数据,他还有一个名字叫做 Unparsed Character Data ,因此 CDATA 对应的文本中的标签会被当做普通文本,不会被解析.

与之相对应的就是术语 PCDATA , PCDATAParsed Character Data 的缩写,表示应该由 XML 解析器解析的文本数据, PCDATA 对应的文本中的标签会被正常解析.

所以,根据 sql 元素上的 PDATA 标记,我们可以大概断定 sql 元素的性质: sql 元素中的文本定义,允许子元素和普通文本混排.

在了解了 sql 元素的基本信息之后,我们正式看一下 sql 元素的解析操作, sql 元素的解析入口在 XMLMapperBuilderconfigurationElement() 方法中:

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 都会通过 MapperBuilderAssistantapplyCurrentNamespace() 方法将其 id 转换为全局唯一的标志.

然后将通过 databaseIdMatchesCurrent() 方法校验的 sql 元素,存放到 XMLMapperBuildersqlFragments 集合中,供后续的解析过程使用.

负责校验 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 属性,在当前尚未有相同 idsql 元素注册进来的前提下,那么该元素就是有效的.

值得注意的是,前面的 sqlElement() 方法调用了两次重载的 sqlElement() 方法,第一次调用时,指定了 requiredDatabaseId 参数,第二次没有指定.

因此,结合着 databaseIdMatchesCurrent() 方法的实现来看,针对具有相同 id 属性的 sql 元素,如果同时匹配了指定 databaseId 和未指定 databaseId 属性的两个 sql 元素,未指定 databaseId 属性的 sql 元素将会被忽略.

这就是关于sql元素的解析过程了,相对来说比较简单,我本打算将动态 sql 相关的内容放到这篇文章中,后来仔细想了想,还是放到后面来说吧.

就酱,告辞!

原文  https://juejin.im/post/5ea53cfa518825738e218fb1
正文到此结束
Loading...