精通Mybatis之动态sql全流程解析

前言

小编的精通mybatis之讲解就快结束了,希望大家坚持不懈坚持到底了。好了进入今天的正题,动态sql全流程。

动态Sql定义

定义:每次构建sql脚本时,根据预先编写的脚本以及参数动态构建可执行的sql。
动态SQL是MyBatis 强大功能之一,他免除了在JAVA代码中拼装SQL字符串麻烦,同时保留了我们对SQL的自主控制,更方便进行SQL性能优化改造。这也是大部分的编程伙伴喜欢用mybatis的原因。
对动态sql的使用大家应该很熟悉了吧,如果有需要大家可以看官网使用动态 SQL。首先小编带大家看下sql脚本元素:

精通Mybatis之动态sql全流程解析_第1张图片
如果你之前用过 JSTL 或任何基于类 XML 语言的文本处理器,你对动态 SQL 元素可能会感觉似曾相识。在 MyBatis 之前的版本中,需要花时间了解大量的元素。借助功能强大的基于 OGNL 的表达式,MyBatis 3 替换了之前的大部分元素,大大精简了元素种类,现在要学习的元素种类就上面这些了。其实官网里面还有bind和接口注解方式的script。
那小编带大家先了解一下OGNL的表达式吧。

OGNL表达式

OGNL全称是对象导航图语言(Object Graph Navigation Language)是一种JAVA表达示语言,可以方便的存取对象属和方法,已用于逻辑判断。其支持以下特性:

  • 获取属性属性值,以及子属性值进行逻辑计算
    id != null || autho.name != null
  • 表达示中可直接调用方法,(如果是无参方法,可以省略括号)
    ! comments.isEmpty && comments.get(0) != null
  • 通过下标访问数组或集合
    comments[0].id != null
    遍历集合
    Iterable comments = evaluator.evaluateIterable(“comments”, blog);

接下来小编用代码示例演示一下
这边使用了官网的示例,Blog ,commit,user三个对象

public class OgnlTest {
     

    @Test
    public void ognlExpressionTest() {
     
        ExpressionEvaluator expressionEvaluator = new ExpressionEvaluator();
        Blog blog = new Blog();
        blog.setId(1);
        User user = new User();
        user.setId(2);
        blog.setAuthor(user);
        List<Comment> commentList = new ArrayList<>();
        Comment comment = new Comment();
        comment.setBody("123");
        commentList.add(comment);
        blog.setComments(commentList);
		//这里author如果为空的话author.id肯定会报错
        boolean hasAuthor = expressionEvaluator.evaluateBoolean("id !=null && author.id!=null", blog);
        System.out.println(hasAuthor);
        boolean hasComments = expressionEvaluator.evaluateBoolean("comments !=null && !comments.isEmpty", blog);
        System.out.println(hasComments);

        boolean hasCommentBody = expressionEvaluator.evaluateBoolean("comments !=null && comments.get(0).body!=null", blog);
        System.out.println(hasCommentBody);
        boolean hasCommentBody2 = expressionEvaluator.evaluateBoolean("comments !=null && comments[0].body!=null", blog);
        System.out.println(hasCommentBody2);

        Iterable<?> comments = expressionEvaluator.evaluateIterable("comments", blog);
        for (Object o : comments) {
     
            System.out.println(o);
        }
    }
}

测试结果:

true
true
true
true
org.coderead.mybatis.bean.Comment@5b1d2887

SqlSource解析过程

说完了OGNL表达式,咱们来说一下sql的解析过程,即从数据源到我们可以执行的sql。SqlSource是sql的数据源,他可以通过注解的方式或xml方式得到对应的源。右边的BoundSql就是可执行sql的所有东西。StatementHandler就是根据他去执行sql的(当然获取他的时候还包装了一层:MappedStatement),待会儿小编会带大家看他的源码,一看就明白了。
精通Mybatis之动态sql全流程解析_第2张图片
小编稍微对sqlSource的实现类讲解一下:

  1. ProviderSqlSource :第三方法SQL源,每次获取SQL都会基于参数动态创建静态数据源,然后在创建BoundSql
  2. DynamicSqlSource:动态SQL源包含了SQL脚本,每次获取SQL都会基于参数以及脚本,动态创建创建BoundSql
  3. RawSqlSource:不包含任何动态元素,原生文本的SQL。但这个SQL是不能直接执行的,需要转换成BoundSql
  4. StaticSqlSource:包含可执行的SQL,以及参数映射,可直接生成BoundSql。前面三个数据源都要先创建StaticSqlSource然后才创建BoundSql。

因为第三方很少涉及,一般我们只是使用静态或动态,静态很简单直接将#{}变成?,然后将参数值设置进去即可(这边在变成问号的时候,参数值映射也是一一对应的,有兴趣的小伙伴可以去看下源码)。如:select * from user where user_id =#{userId},所以小编着重讲一下动态sql源的解析过程。

动态Sql源解析

先看下动态sql源的解析流程。
精通Mybatis之动态sql全流程解析_第3张图片
看到这儿大家是不是很懵,这个小编是根据源码来写的,首先还是得让大家知道什么是SqlNode,以及我们的动态sql是如何和SqlNode建立起关系的。

精通Mybatis之动态sql全流程解析_第4张图片

SqlNode这里使用了解释器模式,小编这里简单的解释一下这个设计模式。
解释器模式(Interpreter Pattern)提供了评估语言的语法或表达式的方式,它属于行为型模式。这种模式实现了一个表达式接口,该接口解释一个特定的上下文。这种模式被用在 SQL 解析、符号处理引擎等。
简单介绍
意图:给定一个语言,定义它的文法表示,并定义一个解释器,这个解释器使用该标识来解释语言中的句子。
主要解决:对于一些固定文法构建一个解释句子的解释器。
何时使用:如果一种特定类型的问题发生的频率足够高,那么可能就值得将该问题的各个实例表述为一个简单语言中的句子。这样就可以构建一个解释器,该解释器通过解释这些句子来解决该问题。
如何解决:构建语法树,定义终结符与非终结符。
关键代码:构建环境类,包含解释器之外的一些全局信息,一般是 HashMap。

SqlNode主要是来解析Mybatis中的Sql脚本元素,之后将解析完毕sql添加到DynamicContext中去。小编简单说下各个SqlNode的作用:

  1. SqlNode是总接口只有一个方法:apply(DynamicContext context),作用如上面小编所讲,各个sqlNode处理完对应的逻辑然后将对应sql添加到DynamicContext
  2. MixedSqlNode包含多个子sqlNode,是个list然后循环调用子节点的逻辑
  3. ChooseSqlNode,IfSqlNode,ForEachSqlNode,TrimSqlNode这些节点就是来处理对应的sql脚本元素
  4. StaticTextSqlNode为静态脚本node,直接拼接的是静态脚本,如:select * from user
  5. TextSqlNode为文本脚本node,他主要是用来替换${}占位符的,直接替换成文本如:select * from ${table_name}

题外话:这边小编想起了一个mybatis的面试题,说${}与#{}替换符的区别,其实结论大家都知道,但是具体实现可能没有真正的看过,有兴趣的小伙伴可以看下。结论是很简单且正确的,但求证的却很少

这些SqlNode是怎样的数据结构才能解析我们的动态sql呢,看过解释器模式就知道他其实构建语法树,怎么构建的看下图:

精通Mybatis之动态sql全流程解析_第5张图片
脚本之间是呈现嵌套关系的。比如if元素中会包含一个MixedSqlNode ,而MixedSqlNode下又会包含1至1至多个其它节点。最后组成一课脚本语法树。如上图左边的SQL元素组成右边的语法树。在节点最底层一定是一个StaticTextNode或 TextNode。
这就是xm中的sqll脚本解析变成语法树的结构。这边小编不一一讲各种元素,这里挑选几个讲一下

if、where、foreach

模拟if,where解析过程:

@Test
    public void sqlNodeTest(){
     
        Company company = new Company();
        company.setId(1L);
        company.setCompanyName("伟大的公司");
        DynamicContext dynamicContext = new DynamicContext(configuration,company);
        StaticTextSqlNode staticTextSqlNode = new StaticTextSqlNode("select * from company");
        staticTextSqlNode.apply(dynamicContext);

        IfSqlNode ifIdSqlNode = new IfSqlNode(new StaticTextSqlNode("id = #{id}"),"id != null");
        IfSqlNode ifNameSqlNode = new IfSqlNode(new StaticTextSqlNode(" and company_name = #{companyName}"),"companyName != null");
        MixedSqlNode mixedSqlNode = new MixedSqlNode(Arrays.asList(ifIdSqlNode,ifNameSqlNode));
        WhereSqlNode whereSqlNode= new WhereSqlNode(configuration,mixedSqlNode);
        whereSqlNode.apply(dynamicContext);
        System.out.println(dynamicContext.getSql());
    }

测试结果:
在这里插入图片描述
这边大家有没有发现,if其实不需要MixedSqlNode,包装一个StaticTextSqlNode即可,为什么小编在上面的脚本树中包含的是MixedSqlNode,这是因为if里面还可以加if所以里面需要包装使用MixedSqlNode,在mybatis实际过程中也是如此。

froeach

@Test
    public void foreachNodeTest(){
     
        Map<String,Object> paramMap = new HashMap<>(1);
        List<Long> idList = Arrays.asList(1L,2L,3L);
        paramMap.put("idList",idList);
        DynamicContext dynamicContext = new DynamicContext(configuration,paramMap);
        StaticTextSqlNode staticTextSqlNode = new StaticTextSqlNode("select * from company where id in");
        ForEachSqlNode forEachSqlNode = new ForEachSqlNode(configuration,
                new MixedSqlNode(Arrays.asList(new StaticTextSqlNode("#{item}"))),"idList","index","item","(",")",",");
        staticTextSqlNode.apply(dynamicContext);
        forEachSqlNode.apply(dynamicContext);
        System.out.println(dynamicContext.getSql());
    }

测试结果:
在这里插入图片描述
看到这个参数的替换是不是不一样啊。这边小编提个问,为什么List的参数小编要转成map参数传进去?
如果不了解的话建议看下精通Mybatis之Jdbc处理器StatementHandler中的参数转换。

小结

其实真正的解析也差不多,小编只是将他们拆开来了,实际过程根据xml里面的sql语句分段然后使用SqlNode拼装成脚本结构树,顶层只有MixedSqlNode 就是根节点,然后在执行的时候根据结构树变成sql。稍微有点区别的是,sqlNode还会进行一次包装。

动态标签xml解析过程

上面是小编是在底层拆开揉碎了展开的,那从用户角度,咱们编写好xml他是如何解析的呢,其实不难,听小编慢慢道来,SqlSource 是基于XML解析而来,解析的底层是使用Dom4j 把XML解析成一个个子节点,在通过 XMLScriptBuilder 遍历这些子节点最后生成对应的Sql源。其解析流程如下图:
精通Mybatis之动态sql全流程解析_第6张图片
nodeHandler 类图:
在这里插入图片描述

源码阅读:
小编编写了稍微复杂的xml的sql然后进行debug调试,测试xml的sql如下:



<mapper namespace="mapper.EmployeeMapper">

    <resultMap id="employMap" type="entity.Employee" autoMapping="true"/>
    <select id="selectByIdListAndName" resultType="collection" resultMap="employMap">
        select * from employee
        <where>
            <if test="idList != null and !idList.isEmpty">
                id in
                <foreach collection="idList" separator="," index="index" item="item" open="(" close=")">
                    #{item}
                foreach>
            if>

            <if test="name != null">
                name = #{name}
            if>
        where>
    select>
mapper>

XMLScriptBuilder源码(太多了小编捡一些重点):

//构造方法,其中context 就是上面的