共计 10751 个字符,预计需要花费 27 分钟才能阅读完成。
前言
Mybatis
提供了弱小的动静 SQL
语句生成性能,以应答简单的业务场景,本篇文章将联合 Mybatis
解析 SQL
语句的过程对 Mybatis
中对 <if>
,<where>
,<foreach>
等动静 SQL
标签的反对进行剖析。
注释
一. XML 文档中的节点概念
在剖析 Mybatis
如何反对 SQL
语句之前,本大节先剖析 XML 文档中的节点概念。XML文档中的每个成分都是一个节点,DOM对 XML 节点的规定如下所示。
- 整个文档是一个 文档节点;
- 每个 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 源码 - 加载映射文件与动静代理中曾经晓得,在 XMLStatementBuilder
的parseStatementNode()
办法中,会解析映射文件中的 <select>
,<insert>
,<update>
和<delete>
标签(后续对立称为 CURD 标签),并生成 MappedStatement
而后缓存到 Configuration
中。CURD标签的解析由 XMLLanguageDriver
实现,每个标签解析之后会生成一个 SqlSource
,能够了解为SQL
语句,本大节将对 XMLLanguageDriver
如何实现 CURD 标签的解析进行探讨。
XMLLanguageDriver
创立 SqlSource
的createSqlSource()
办法如下所示。
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 标签对应的节点缓存起来,而后初始化 nodeHandlerMap,nodeHandlerMap 中寄存着解决 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());
}
当初剖析 XMLScriptBuilder
的parseScriptNode()
办法,该办法会创立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;
}
在 XMLScriptBuilder
的parseScriptNode()
办法中,会依据 XMLScriptBuilder
中的 isDynamic 属性判断是创立 DynamicSqlSource
还是 RawSqlSource
,在这里临时不剖析DynamicSqlSource
与RawSqlSource
的区别,然而能够揣测在 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 中并设置 isDynamic 为true,如果文本值不蕴含 ${}
占位符,则创立 StaticTextSqlNode
并增加到 contents 中。如果 CURD 标签节点的子节点是元素节点时,因为 CURD 标签节点的元素节点只可能为 <if>
,<foreach>
等动静 SQL
标签节点,所以间接会设置 isDynamic 为true,同时还会调用动静 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()
办法,大抵能够晓得 MixedSqlNode
和IfSqlNode
也实现了 SqlNode
接口,上面看一下 MixedSqlNode
和IfSqlNode
的实现,如下所示。
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
赋值给 IfSqlNode
的contents。
不同的 SqlNode
都是能够蕴含彼此的,这是 组合设计模式 的利用,SqlNode
之间的关系如下所示。
SqlNode
接口定义了一个办法,如下所示。
public interface SqlNode {boolean apply(DynamicContext context);
}
每个 SqlNode
的apply()
办法中,除了实现本人自身的逻辑外,还会调用本人所持有的所有 SqlNode
的apply()
办法,最终逐层调用上来,所有 SqlNode
的apply()
办法均会被执行。
当初回到 XMLScriptBuilder
的parseScriptNode()
办法,该办法中会调用 parseDynamicTags()
办法以解析 CURD 标签节点并失去 MixedSqlNode
,MixedSqlNode
中含有被解析的 CURD 标签节点的所有子节点对应的 SqlNode
,最初会基于MixedSqlNode
创立 DynamicSqlSource
或者 RawSqlSource
,如果CURD 标签中含有动静 SQL
标签或者 SQL
语句中含有 ${}
占位符,则创立 DynamicSqlSource
,否则创立RawSqlSource
。上面别离对DynamicSqlSource
和RawSqlSource
的实现进行剖析。
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
中的 SqlNode
的apply()
办法以实现动静 SQL
语句的生成,此时生成的 SQL
语句中的占位符(如果有的话)为 #{}
,而后再调用SqlSourceBuilder
的parse()
办法将 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);
}
}
所以剖析到这里,能够晓得 DynamicSqlSource
的getBoundSql()
办法实际上会实现动静 SQL
语句的生成和 #{}
占位符替换,而后基于生成好的 SQL
语句创立 BoundSql
并返回。BoundSql
对象的类图如下所示。
实际上,Mybatis
中执行 SQL
语句时,如果映射文件中的 SQL
应用到了动静 SQL
标签,那么 Mybatis
中的 Executor
(执行器,后续文章中会进行介绍)会调用MappedStatement
的getBoundSql()
办法,而后在 MappedStatement
的getBoundSql()
办法中又会调用 DynamicSqlSource
的getBoundSql()
办法,所以 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
语句,则会生成 DynamicSqlSource
。Mybatis
在生成 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
语句。