详解MyBatis的动态SQL实现原理
前言
MyBatis版本:3.5.6
正文
一. 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>元素节点的上一个兄弟节点为文本节点,下一个兄弟节点也为文本节点。
二. 动态SQL解析流程说明
整体的一个解析流程如下所示。
也就是写在映射文件中的一条SQL
,会最终被解析为DynamicSqlSource
或者RawSqlSource
,前者表示动态SQL
,后者表示静态SQL
。
上图中的MixedSqlNode,其通常的包含关系可以由下图定义。
也就是映射文件中定义一条SQL语句的CRUD标签里的各种子元素,均会被解析为一个SqlNode,比如包含了${}
的文本,会被解析为TextSqlNode,不包含${}
的文本,会被解析为StaticTextSqlNode,<choose>标签会被解析为ChooseSqlNode等,同时又因为<choose>标签中会再有<when>和<otherwise>子标签,所以ChooseSqlNode中又会持有这些子标签的SqlNode。
所以一条SQL
最终就是由这条SQL
对应的CRUD
标签解析成的各种SqlNode
组合而成。
三. 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() 方法均会被执行。
四. DynamicSqlSource和RawSqlSource源码分析
回到XMLScriptBuilder的parseScriptNode() 方法,该方法中会调用parseDynamicTags() 方法以解析CURD标签节点并得到MixedSqlNode,MixedSqlNode中含有被解析的CURD标签节点的所有子节点对应的SqlNode,最后会基于MixedSqlNode创建DynamicSqlSource或者RawSqlSource,如果CURD标签中含有动态SQL标签或者SQL语句中含有${}
占位符,则创建DynamicSqlSource,否则创建RawSqlSource。下面分别对DynamicSqlSource和RawSqlSource的实现进行分析。
1. DynamicSqlSource源码分析
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语句会在这条语句实际要执行时才会生成。
2. RawSqlSource源码分析
现在看一下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
语句。
以上就是详解MyBatis的动态SQL实现原理的详细内容,更多关于MyBatis 动态SQL实现的资料请关注脚本之家其它相关文章!
相关文章
springboot打war包部署到外置tomcat容器的方法
这篇文章主要介绍了springboot]打war包部署到外置tomcat容器,在这需要注意的是在boot-launch.war在tomcat webapps目录里面解压到boot-launch文件夹,感兴趣的朋友跟随小编一起看看吧2022-04-04
最新评论