关于mybatis:Mybatis源码动态SQL的实现原理

53次阅读

共计 10751 个字符,预计需要花费 27 分钟才能阅读完成。

前言

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 语句。

正文完
 0