美文网首页Mybatis源码之美Mybatis
Mybatis源码之美:3.6.解析sql代码块

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

作者: 吃竹子的程序熊 | 来源:发表于2020-04-26 15:48 被阅读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相关的内容放到这篇文章中,后来仔细想了想,还是放到后面来说吧.

    就酱,告辞!

    相关文章

      网友评论

        本文标题:Mybatis源码之美:3.6.解析sql代码块

        本文链接:https://www.haomeiwen.com/subject/dpqjwhtx.html