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