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

Posted 木兮君

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了精通Mybatis之动态sql全流程解析相关的知识,希望对你有一定的参考价值。

前言

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

动态Sql定义

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

在这里插入图片描述
如果你之前用过 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),待会儿小编会带大家看他的源码,一看就明白了。
在这里插入图片描述
小编稍微对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源的解析流程。
在这里插入图片描述
看到这儿大家是不是很懵,这个小编是根据源码来写的,首先还是得让大家知道什么是SqlNode,以及我们的动态sql是如何和SqlNode建立起关系的。

在这里插入图片描述

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呢,看过解释器模式就知道他其实构建语法树,怎么构建的看下图:

在这里插入图片描述
脚本之间是呈现嵌套关系的。比如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源。其解析流程如下图:
在这里插入图片描述
nodeHandler 类图:
在这里插入图片描述

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

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<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 就是上面的<select>标签的所有内容,parameterType为属性值的class类型
public XMLScriptBuilder(Configuration configuration, XNode context, Class<?> parameterType) {
    super(configuration);
    this.context = context;
    this.parameterType = parameterType;
    initNodeHandlerMap();
  }
  //初始化NodeHandlerMap,处理各脚本元素的handler,各节点最后是相应的sqlNode
  //然后添加到rootNode的contexts列表中(rootNode 类型是MixedSqlNode,
  //而contexts为MixedSqlNode中的属性 类型为List<SqlNode>)  
private void initNodeHandlerMap() {
    nodeHandlerMap.put("trim", new TrimHandler());
    nodeHandlerMap.put("where", new WhereHandler());
    nodeHandlerMap.put("set", new SetHandler());
    nodeHandlerMap.put("foreach", new ForEachHandler());
    nodeHandlerMap.put("if", new IfHandler());
    nodeHandlerMap.put("choose", new ChooseHandler());
    nodeHandlerMap.put("when", new IfHandler());
    nodeHandlerMap.put("otherwise", new OtherwiseHandler());
    nodeHandlerMap.put("bind", new BindHandler());
  }
//开始解析
  public SqlSource parseScriptNode() {
    MixedSqlNode rootSqlNode = parseDynamicTags(context);
    SqlSource sqlSource;
    if (isDynamic) {
       //如果是动态的就包装成动态的sqlsource
      sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
    } else {
     //否则就是静态sqlSource
      sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
    }
    return sqlSource;
  }
  
  protected MixedSqlNode parseDynamicTags(XNode node) {
    //创建sqlNode子节点树
    List<SqlNode> contents = new ArrayList<>();
    //拿到子节点
    NodeList children = node.getNode().getChildNodes();
    //遍历子节点
    for (int i = 0; i < children.getLength(); i++) {
      XNode child = node.newXNode(children.item(i));
      //拿到各个节点的数据,首先判断是否是文本数据
      if (child.getNode().getNodeType() == Node.CDATA_SECTION_NODE || child.getNode().getNodeType() == Node.TEXT_NODE) {
        String data = child.getStringBody("");
        TextSqlNode textSqlNode = new TextSqlNode(data);
        //判断是否是脚本元素 动态sql
        if (textSqlNode.isDynamic()) {
          contents.add(textSqlNode);
          isDynamic = true;
        } else {
          contents.add(new StaticTextSqlNode(data));
        }
        //如果是元素node
      } else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) { // issue #628
      	//取出node的名称
        String nodeName = child.getNode().getNodeName();
        //更加元素node取出对应的NodeHandler
        NodeHandler handler = nodeHandlerMap.get(nodeName);
        if (handler == null) {
          throw new BuilderException("Unknown element <" + nodeName + "> in SQL statement.");
        }
        //根据对应的nodeHandler处理对应的node节点
        handler.handleNode(child, contents);
        isDynamic = true;
      }
    }
    //根节点始终为MixedSqlNode
    return new MixedSqlNode(contents);
  }

NodeHandler源码:这是XMLScriptBuilder的内部接口和内部实现(下面比较简单,不停递归调用最后扔进相应的sqlNode即可)

private interface NodeHandler {
    void handleNode(XNode nodeToHandle, List<SqlNode> targetContents);
  }

  private class BindHandler implements NodeHandler {
    public BindHandler() {
      // Prevent Synthetic Access
    }

    @Override
    public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
      final String name = nodeToHandle.getStringAttribute("name");
      final String expression = nodeToHandle.getStringAttribute("value");
      final VarDeclSqlNode node = new VarDeclSqlNode(name, expression);
      targetContents.add(node);
    }
  }

  private class TrimHandler implements NodeHandler {
    public TrimHandler() {
      // Prevent Synthetic Access
    }

    @Override
    public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
      MixedSqlNode mixedSqlNode = parseDynamicTags(nodeToHandle);
      String prefix = nodeToHandle.getStringAttribute("prefix");
      String prefixOverrides = nodeToHandle.getStringAttribute("prefixOverrides");
      String suffix = nodeToHandle.getStringAttribute("suffix");
      String suffixOverrides = nodeToHandle.getStringAttribute("suffixOverrides");
      TrimSqlNode trim = new TrimSqlNode(configuration, mixedSqlNode, prefix, prefixOverrides, suffix, suffixOverrides);
      targetContents.add(trim);
    }
  }

  private class WhereHandler implements NodeHandler {
    public WhereHandler() {
      // Prevent Synthetic Access
    }

    @Override
    public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
      MixedSqlNode mixedSqlNode = parseDynamicTags(nodeToHandle);
      WhereSqlNode where = new WhereSqlNode(configuration, mixedSqlNode);
      targetContents.add(where);
    }
  }

  private class SetHandler implements NodeHandler {
    public SetHandler() {
      // Prevent Synthetic Access
    }

    @Override
    public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
      MixedSqlNode mixedSqlNode = parseDynamicTags(nodeToHandle);
      SetSqlNode set = new SetSqlNode(configuration, mixedSqlNode);
      targetContents.add(set);
    }
  }

  private class ForEachHandler implements NodeHandler {
    public ForEachHandler() {
      // Prevent Synthetic Access
    }

    @Override
    public void handleNode(XNode nodeToHandle, List<SqlNodeMyBatis从入门到精通:MyBatis动态Sql之if标签的用法

Mybatis -- 动态Sql概述动态Sql之<if>(包含<where>)动态Sql之<foreach>sql片段抽取

精通Mybatis之结果集处理流程与映射体系(无死角懒加载讲解)

Mybatis超强大的动态SQL大全

Mybatis超强大的动态SQL大全

MyBatis:动态sql语句