前言
Mybatis
提供了强大的动态SQL
语句生成功能,以应对复杂的业务场景,本篇文章将结合Mybatis
解析SQL
语句的过程对Mybatis
中对
,
,
等动态SQL
标签的支持进行分析。
正文
一. XML文档中的节点概念
在分析Mybatis
如何支持SQL
语句之前,本小节先分析XML文档中的节点概念。XML文档中的每个成分都是一个节点,DOM对XML节点的规定如下所示。
- 整个文档是一个文档节点;
- 每个XML标签是一个元素节点;
- 包含在元素节点中的文本是文本节点。
以一个XML文档进行说明,如下所示。
成都
武汉
如上所示,整个XML文档是一个文档节点,这个文档节点有一个子节点,就是
元素节点,
元素节点有五个子节点,分别是:文本节点,
元素节点,文本节点,
元素节点和文本节点,注意,在
元素节点的子节点中的文本节点的文本值均是\n,表示换行符。同样,
元素节点有三个子节点,分别是:文本节点,
元素节点和文本节点,这里的文本节点的文本值也是\n,然后
元素节点只有一个子节点,为一个文本节点。节点的子节点之间互为兄弟节点,例如
元素的五个子节点之间互为兄弟节点,name为“四川”的
元素节点的上一个兄弟节点为文本节点,下一个兄弟节点也为文本节点。
二. Mybatis支持动态SQL源码分析
在Mybatis源码-加载映射文件与动态代理中已经知道,在XMLStatementBuilder
的parseStatementNode()
方法中,会解析映射文件中的,
,
和
标签(后续统一称为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 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节点的子节点中的元素节点只可能为,等动态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标签节点的元素节点只可能为
,
等动态SQL
标签节点,所以直接会设置isDynamic为true,同时还会调用动态SQL
标签节点对应的处理器来生成SqlNode
并添加到contents中。这里以
标签节点对应的处理器的handleNode()
方法为例进行说明,如下所示。
public void handleNode(XNode nodeToHandle, List targetContents) {
//递归调用parseDynamicTags()解析标签节点
MixedSqlNode mixedSqlNode = parseDynamicTags(nodeToHandle);
String test = nodeToHandle.getStringAttribute("test");
//创建IfSqlNode
IfSqlNode ifSqlNode = new IfSqlNode(mixedSqlNode, test);
//将IfSqlNode添加到contents中
targetContents.add(ifSqlNode);
}
在
标签节点对应的处理器的handleNode()
方法中,递归的调用了parseDynamicTags()
方法来解析
标签节点,例如
,
等标签节点对应的处理器的handleNode()
方法中也会递归调用parseDynamicTags()
方法,这是因为这些动态SQL
标签是可以嵌套使用的,比如
标签节点的子节点可以为
标签节点。通过上面的handleNode()
方法,大致可以知道MixedSqlNode
和IfSqlNode
也实现了SqlNode
接口,下面看一下MixedSqlNode
和IfSqlNode
的实现,如下所示。
public class MixedSqlNode implements SqlNode {
private final List contents;
public MixedSqlNode(List 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 parameterMappings;
private final Configuration configuration;
public StaticSqlSource(Configuration configuration, String sql) {
this(configuration, sql, null);
}
public StaticSqlSource(Configuration configuration, String sql,
List 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
语句。