前言

Mybatis提供了弱小的动静SQL语句生成性能,以应答简单的业务场景,本篇文章将联合Mybatis解析SQL语句的过程对Mybatis中对<if><where><foreach>等动静SQL标签的反对进行剖析。

注释

一. XML文档中的节点概念

在剖析Mybatis如何反对SQL语句之前,本大节先剖析XML文档中的节点概念。XML文档中的每个成分都是一个节点,DOMXML节点的规定如下所示。

  • 整个文档是一个文档节点
  • 每个XML标签是一个元素节点
  • 蕴含在元素节点中的文本是文本节点

以一个XML文档进行阐明,如下所示。

<provinces>    <province name="四川">        <capital>成都</capital>    </province>    <province name="湖北">        <capital>武汉</capital>    </province></provinces>

如上所示,整个XML文档是一个文档节点,这个文档节点有一个子节点,就是<provinces>元素节点,<provinces>元素节点有五个子节点,别离是:文本节点,<province>元素节点,文本节点,<province>元素节点和文本节点,留神,在<provinces>元素节点的子节点中的文本节点的文本值均是\n,示意换行符。同样,<province>元素节点有三个子节点,别离是:文本节点,<capital>元素节点和文本节点,这里的文本节点的文本值也是\n,而后<capital>元素节点只有一个子节点,为一个文本节点。节点的子节点之间互为兄弟节点,例如<provinces>元素的五个子节点之间互为兄弟节点,name为“四川”的<province>元素节点的上一个兄弟节点为文本节点,下一个兄弟节点也为文本节点。

二. Mybatis反对动静SQL源码剖析

在Mybatis源码-加载映射文件与动静代理中曾经晓得,在XMLStatementBuilderparseStatementNode()办法中,会解析映射文件中的<select><insert><update><delete>标签(后续对立称为CURD标签),并生成MappedStatement而后缓存到Configuration中。CURD标签的解析由XMLLanguageDriver实现,每个标签解析之后会生成一个SqlSource,能够了解为SQL语句,本大节将对XMLLanguageDriver如何实现CURD标签的解析进行探讨。

XMLLanguageDriver创立SqlSourcecreateSqlSource()办法如下所示。

public SqlSource createSqlSource(Configuration configuration,         XNode script, Class<?> parameterType) {    XMLScriptBuilder builder = new XMLScriptBuilder(            configuration, script, parameterType);    return builder.parseScriptNode();}

如上所示,createSqlSource()办法的入参中,XNode就是CURD标签对应的节点,在createSqlSource()办法中先是创立了一个XMLScriptBuilder,而后通过XMLScriptBuilder来生成SqlSource。先看一下XMLScriptBuilder的构造方法,如下所示。

public XMLScriptBuilder(Configuration configuration, XNode context,                     Class<?> parameterType) {    super(configuration);    this.context = context;    this.parameterType = parameterType;    initNodeHandlerMap();}

XMLScriptBuilder的构造方法中,次要是将CURD标签对应的节点缓存起来,而后初始化nodeHandlerMapnodeHandlerMap中寄存着解决Mybatis提供的反对动静SQL的标签的处理器,initNodeHandlerMap()办法如下所示。

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());}

当初剖析XMLScriptBuilderparseScriptNode()办法,该办法会创立SqlSource,如下所示。

public SqlSource parseScriptNode() {    //解析动静标签    MixedSqlNode rootSqlNode = parseDynamicTags(context);    SqlSource sqlSource;    if (isDynamic) {        //创立DynamicSqlSource并返回        sqlSource = new DynamicSqlSource(configuration, rootSqlNode);    } else {        //创立RawSqlSource并返回        sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);    }    return sqlSource;}

XMLScriptBuilderparseScriptNode()办法中,会依据XMLScriptBuilder中的isDynamic属性判断是创立DynamicSqlSource还是RawSqlSource,在这里临时不剖析DynamicSqlSourceRawSqlSource的区别,然而能够揣测在parseDynamicTags()办法中会扭转isDynamic属性的值,即在parseDynamicTags()办法中会依据CURD标签的节点生成一个MixedSqlNode,同时还会扭转isDynamic属性的值以批示以后CURD标签中的SQL语句是否是动静的。MixedSqlNode是什么,isDynamic属性值在什么状况下会变为true,带着这些疑难,持续看parseDynamicTags()办法,如下所示。

protected MixedSqlNode parseDynamicTags(XNode node) {    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 textSqlNode = new TextSqlNode(data);            //isDynamic()办法能够判断文本节点值是否有${}占位符            if (textSqlNode.isDynamic()) {                //文本节点值有${}占位符                //增加TextSqlNode到汇合中                contents.add(textSqlNode);                //设置isDynamic为true                isDynamic = true;            } else {                //文本节点值没有占位符                //创立StaticTextSqlNode并增加到汇合中                contents.add(new StaticTextSqlNode(data));            }        } else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) {            //子节点为元素节点            //CURD节点的子节点中的元素节点只可能为<if>,<foreach>等动静Sql标签节点            String nodeName = child.getNode().getNodeName();            //依据动静Sql标签节点的名称获取对应的处理器            NodeHandler handler = nodeHandlerMap.get(nodeName);            if (handler == null) {                throw new BuilderException("Unknown element <" + nodeName + "> in SQL statement.");            }            //解决动静Sql标签节点            handler.handleNode(child, contents);            //设置isDynamic为true            isDynamic = true;        }    }    //创立MixedSqlNode    return new MixedSqlNode(contents);}

依照失常执行流程调用parseDynamicTags()时,入参是CURD标签节点,此时会遍历CURD标签节点的所有子节点,基于每个子节点都会创立一个SqlNode而后增加到SqlNode的汇合contents中,最初将contents作为入参创立MixedSqlNode并返回。SqlNode是一个接口,在parseDynamicTags()办法中,能够晓得,TextSqlNode实现了SqlNode接口,StaticTextSqlNode实现了SqlNode接口,所以当节点的子节点是文本节点时,如果文本值蕴含有${}占位符,则创立TextSqlNode增加到contents中并设置isDynamictrue,如果文本值不蕴含${}占位符,则创立StaticTextSqlNode并增加到contents中。如果CURD标签节点的子节点是元素节点时,因为CURD标签节点的元素节点只可能为<if><foreach>等动静SQL标签节点,所以间接会设置isDynamictrue,同时还会调用动静SQL标签节点对应的处理器来生成SqlNode并增加到contents中。这里以<if>标签节点对应的处理器的handleNode()办法为例进行阐明,如下所示。

public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {    //递归调用parseDynamicTags()解析<if>标签节点    MixedSqlNode mixedSqlNode = parseDynamicTags(nodeToHandle);    String test = nodeToHandle.getStringAttribute("test");    //创立IfSqlNode    IfSqlNode ifSqlNode = new IfSqlNode(mixedSqlNode, test);    //将IfSqlNode增加到contents中    targetContents.add(ifSqlNode);}

<if>标签节点对应的处理器的handleNode()办法中,递归的调用了parseDynamicTags()办法来解析<if>标签节点,例如<where><foreach>等标签节点对应的处理器的handleNode()办法中也会递归调用parseDynamicTags()办法,这是因为这些动静SQL标签是能够嵌套应用的,比方<where>标签节点的子节点能够为<if>标签节点。通过下面的handleNode()办法,大抵能够晓得MixedSqlNodeIfSqlNode也实现了SqlNode接口,上面看一下MixedSqlNodeIfSqlNode的实现,如下所示。

public class MixedSqlNode implements SqlNode {    private final List<SqlNode> contents;    public MixedSqlNode(List<SqlNode> contents) {        this.contents = contents;    }    @Override    public boolean apply(DynamicContext context) {        contents.forEach(node -> node.apply(context));        return true;    }    }public class IfSqlNode implements SqlNode {    private final ExpressionEvaluator evaluator;    private final String test;    private final SqlNode contents;    public IfSqlNode(SqlNode contents, String test) {        this.test = test;        this.contents = contents;        this.evaluator = new ExpressionEvaluator();    }    @Override    public boolean apply(DynamicContext context) {        if (evaluator.evaluateBoolean(test, context.getBindings())) {            contents.apply(context);            return true;        }        return false;    }}

其实到这里曾经逐步清晰明了了,依照失常执行流程调用parseDynamicTags()办法时,是为了将CURD标签节点的所有子节点依据子节点类型生成不同的SqlNode并放在MixedSqlNode中,而后将MixedSqlNode返回,然而CURD标签节点的子节点中如果存在动静SQL标签节点,因为这些动静SQL标签节点也会有子节点,所以此时会递归的调用parseDynamicTags()办法,以解析动静SQL标签节点的子节点,同样会将这些子节点生成SqlNode并放在MixedSqlNode中而后将MixedSqlNode返回,递归调用parseDynamicTags()办法时失去的MixedSqlNode会保留在动静SQL标签节点对应的SqlNode中,比方IfSqlNode中就会将递归调用parseDynamicTags()生成的MixedSqlNode赋值给IfSqlNodecontents

不同的SqlNode都是能够蕴含彼此的,这是组合设计模式的利用,SqlNode之间的关系如下所示。

SqlNode接口定义了一个办法,如下所示。

public interface SqlNode {      boolean apply(DynamicContext context);}

每个SqlNodeapply()办法中,除了实现本人自身的逻辑外,还会调用本人所持有的所有SqlNodeapply()办法,最终逐层调用上来,所有SqlNodeapply()办法均会被执行。

当初回到XMLScriptBuilderparseScriptNode()办法,该办法中会调用parseDynamicTags()办法以解析CURD标签节点并失去MixedSqlNodeMixedSqlNode中含有被解析的CURD标签节点的所有子节点对应的SqlNode,最初会基于MixedSqlNode创立DynamicSqlSource或者RawSqlSource,如果CURD标签中含有动静SQL标签或者SQL语句中含有${}占位符,则创立DynamicSqlSource,否则创立RawSqlSource。上面别离对DynamicSqlSourceRawSqlSource的实现进行剖析。

DynamicSqlSource的实现如下所示。

public class DynamicSqlSource implements SqlSource {    private final Configuration configuration;    private final SqlNode rootSqlNode;    public DynamicSqlSource(Configuration configuration, SqlNode rootSqlNode) {        //构造函数只是进行了简略的赋值操作        this.configuration = configuration;        this.rootSqlNode = rootSqlNode;    }    @Override    public BoundSql getBoundSql(Object parameterObject) {        DynamicContext context = new DynamicContext(configuration, parameterObject);        //调用SqlNode的apply()办法实现Sql语句的生成        rootSqlNode.apply(context);        //SqlSourceBuilder能够将Sql语句中的#{}占位符替换为?        SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);        Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass();        //将Sql语句中的#{}占位符替换为?,并生成一个StaticSqlSource        SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());        //StaticSqlSource中保留有动静生成好的Sql语句,并且#{}占位符全副替换成了?        BoundSql boundSql = sqlSource.getBoundSql(parameterObject);        //生成有序参数映射列表        context.getBindings().forEach(boundSql::setAdditionalParameter);        return boundSql;    }}

DynamicSqlSource的构造函数只是进行了简略的赋值操作,重点在于其getBoundSql()办法,在getBoundSql()办法中,先是调用DynamicSqlSource中的SqlNodeapply()办法以实现动静SQL语句的生成,此时生成的SQL语句中的占位符(如果有的话)为#{},而后再调用SqlSourceBuilderparse()办法将SQL语句中的占位符从#{}替换为?并基于替换占位符后的SQL语句生成一个StaticSqlSource并返回,这里能够看一下StaticSqlSource的实现,如下所示。

public class StaticSqlSource implements SqlSource {    private final String sql;    private final List<ParameterMapping> parameterMappings;    private final Configuration configuration;    public StaticSqlSource(Configuration configuration, String sql) {        this(configuration, sql, null);    }    public StaticSqlSource(Configuration configuration, String sql,                            List<ParameterMapping> parameterMappings) {        //构造函数只是进行简略的赋值操作        this.sql = sql;        this.parameterMappings = parameterMappings;        this.configuration = configuration;    }    @Override    public BoundSql getBoundSql(Object parameterObject) {        //基于Sql语句创立一个BoundSql并返回        return new BoundSql(configuration, sql, parameterMappings, parameterObject);    }}

所以剖析到这里,能够晓得DynamicSqlSourcegetBoundSql()办法实际上会实现动静SQL语句的生成和#{}占位符替换,而后基于生成好的SQL语句创立BoundSql并返回。BoundSql对象的类图如下所示。

实际上,Mybatis中执行SQL语句时,如果映射文件中的SQL应用到了动静SQL标签,那么Mybatis中的Executor(执行器,后续文章中会进行介绍)会调用MappedStatementgetBoundSql()办法,而后在MappedStatementgetBoundSql()办法中又会调用DynamicSqlSourcegetBoundSql()办法,所以Mybatis中的动静SQL语句会在这条语句理论要执行时才会生成。

当初看一下RawSqlSource的实现,如下所示。

public class RawSqlSource implements SqlSource {    private final SqlSource sqlSource;    public RawSqlSource(Configuration configuration, SqlNode rootSqlNode, Class<?> parameterType) {        //先调用getSql()办法获取Sql语句        //而后再执行构造函数        this(configuration, getSql(configuration, rootSqlNode), parameterType);    }    public RawSqlSource(Configuration configuration, String sql, Class<?> parameterType) {        SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);        Class<?> clazz = parameterType == null ? Object.class : parameterType;        //将Sql语句中的#{}占位符替换为?,生成一个StaticSqlSource并赋值给sqlSource        sqlSource = sqlSourceParser.parse(sql, clazz, new HashMap<>());    }    private static String getSql(Configuration configuration, SqlNode rootSqlNode) {        DynamicContext context = new DynamicContext(configuration, null);        rootSqlNode.apply(context);        return context.getSql();    }    @Override    public BoundSql getBoundSql(Object parameterObject) {        //理论是调用StaticSqlSource的getBoundSql()办法        return sqlSource.getBoundSql(parameterObject);    }}

RawSqlSource会在构造函数中就将SQL语句生成好并替换#{}占位符,在SQL语句理论要执行时,就间接将生成好的SQL语句返回。所以Mybatis中,动态SQL语句的执行通常要快于动静SQL语句的执行,这在RawSqlSource类的正文中也有提及,如下所示。

Static SqlSource. It is faster than {@link DynamicSqlSource} because mappings are calculated during startup.

总结

Mybatis会为映射文件中的每个CURD标签节点里的SQL语句生成一个SqlSource,如果是动态SQL语句,那么会生成RawSqlSource,如果是动静SQL语句,则会生成DynamicSqlSourceMybatis在生成SqlSource时,会为CURD标签节点的每个子节点都生成一个SqlNode,无论子节点是文本值节点还是动静SQL元素节点,最终所有子节点对应的SqlNode都会放在SqlSource中以供生成SQL语句应用。如果是动态SQL语句,那么在创立RawSqlSource时就会应用SqlNode实现SQL语句的生成以及将SQL语句中的#{}占位符替换为?,而后保留在RawSqlSource中,等到这条动态SQL语句要被执行时,就间接返回这条动态SQL语句。如果是动静SQL语句,在创立DynamicSqlSource时只会简略的将SqlNode保留下来,等到这条动静SQL语句要被执行时,才会应用SqlNode实现SQL语句的生成以及将SQL语句中的#{}占位符替换为?,最初返回SQL语句,所以Mybatis中,动态SQL语句执行要快于动静SQL语句。