Mybatis源码之美Mybatis

Mybatis源码之美:3.6.解析sql代码块

2020-04-26  本文已影响0人  吃竹子的程序熊

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

就酱,告辞!

上一篇下一篇

猜你喜欢

热点阅读