mybatis 3.5.5的版本
在上一篇xml解析过程完成之后我们知道最重要的具体的sql信息放到了SqlSource中,那么今天就来探究SqlSource接口。
xml文件解析过程: https://blog.youkuaiyun.com/zxzfcsu/article/details/105999047
一、SqlSource接口
package org.apache.ibatis.mapping;
/**
* Represents the content of a mapped statement read from an XML file or an annotation.
* It creates the SQL that will be passed to the database out of the input parameter received from the user.
*/
public interface SqlSource {
BoundSql getBoundSql(Object parameterObject);
}
首先我们看一下作者对于这个接口的解释,第一句话说明这个类表示的就是从xml文件或者注解中读到的mapped statement的内容,第二句话说明这个SqlSource接口的作用就是创建一个SQL语句,那么这个SQL语句会根据用户那的输入参数传入到数据库。
我们看到了SqlSource的具体结构,其实就提供了一个方法叫做getBoundSql,那么很好理解的就是它提供了创建SQL语句的功能,所以也很好理解的明白这个BoundSql其实就是已经整理好的SQL语句,那么这个方法的参数就是用户要传入的参数,也即第二句话中的parameter。
既然是接口我们具体来看一下它的实现类和结构。
其他还有两个实现类分别是ProviderSqlSource和VelocitySqlSource,我们暂不关心,我们主要关注的就是这三个实现类。
二、不同实现类的区别(DynamicSqlSource与RawSqlSource)
1.首先是作用范围(存储对象)上的不同,因为mybatis的sql语句有用“#{}”和“${}”两种写法,而且还有带有动态sql的支持,所以不可能通过一种SqlSource对所有写法的数据进行支持,所以会有这些分类。
DynamicSqlSource存储的是写有“${}”或者具有动态sql标签的sql信息,所以凡是这一类的都创建为DynamicSqlSource。
RawSqlSource则相反,存储的是只有“#{}”或者没有标签的纯文本sql信息。
那么这时候很自然地就会有个疑问,两者互补了还需要StaticSqlSource干什么呢?别急,接下来继续看。
2.再者是解析时机的不同(3.4中进行了验证),因为mybatis是读取的通过标签化的xml文件来得到的sql信息的,与jdbc能使用的sql语句的之间会存在一个解析(转化)的过程,那么这个过程不同的SqlSource有着不同的时机去做。
首先我们看一下RawSqlSource的作者的注释:
Static SqlSource. It is faster than {@link DynamicSqlSource} because mappings are calculated during startup.
那么看到这里就理解了RawSqlSource的解析时机了,它是在startup的时候进行解析的,那么因此在使用的时候会比DynamicSqlSource快。
那么为什么RawSqlSource能够在启动的时候进行解析呢?因为它只保存着“#{}”类型的sql信息,甚至连“#{}”都没有是纯文本,那么这个时候自然可以将“#{}”替换为“?”传入到jdbc的statement中,然后再进行set。而DynamicSqlSource其中比较复杂,无论是“${}”还是动态sql标签都不能在传入parameter之前用占位符进行替换。
那么具体DynamicSqlSource在什么时候进行解析呢,答案就是在使用的时候才会进行解析(见下面代码),即在调用getBoundSql的时候进行解析。那么这样一来,每次调用getBoundSql都会重新解析后返回。而RawSqlSource每次调用getBoundSql方法只需要返回已经解析的sql就可以了,所以两者的速度不同。
//DynamicSqlSource
public BoundSql getBoundSql(Object parameterObject) {
DynamicContext context = new DynamicContext(configuration, parameterObject);
//解析根节点
rootSqlNode.apply(context);
SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass();
SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());
BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
context.getBindings().forEach(boundSql::setAdditionalParameter);
return boundSql;
}
3.不同实现类的属性(结构)不同。DynamicSqlSource类的属性有Configuration和SqlNode,而RawSqlSource中持有另一个SqlSource。那么很好理解的是DynamicSqlSource,存一个Configuration对象留着用(因为所有信息都包含在这里面了),一个root的SqlNode储存节点信息(SqlNode本系列其他文章会总结)。但是RawSqlSource持有另一个SqlSource对象是做什么?我们通过(三)解释SqlSource的构建过程来进行分析。
三、SqlSource的构建过程
在(二)中我们分析了DynamicSqlSource和RawSqlSource的区别,那么我们在这里找一下StaticSqlSource的用处。
1.首先我们回忆一下首次出现SqlSource的是在哪里,可以参见系列文章第一篇末尾:
/**
* 出现在XMLStatementBuilder类的parseStatementNode方法中
* LanguageDriver langDriver 的默认实现类为org.apache.ibatis.scripting.xmltags.XmlLanguageDriver
* XNode context为select|insert|update|delete标签节点
* Class<?> parameterTypeClass是parameterType值所解析出来的类
*/
SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
2.那么我们再去看看XmlLanguageDriver的createSqlSource方法:
public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) {
XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType);
return builder.parseScriptNode();
}
那么实际上交给了一个XMLScriptBuilder去解析这个标签,我们来看看这个类。
3.XMLScriptBuilder这个类同第一篇的众多类一样同样是继承自BaseBuilder,是用于解析具体标签的,下面我们看其中的主要代码:
public class XMLScriptBuilder extends BaseBuilder {
private final XNode context;
private boolean isDynamic;
private final Class<?> parameterType;
//不同节点用不同的handler来处理,用Map的方式来存储对应关系
private final Map<String, NodeHandler> nodeHandlerMap = new HashMap<>();
public XMLScriptBuilder(Configuration configuration, XNode context, Class<?> parameterType) {
super(configuration);
this.context = context;
this.parameterType = parameterType;
initNodeHandlerMap();
}
private void initNodeHandlerMap() {
nodeHandlerMap.put("where", new WhereHandler());
//省略中间的一些handler,这个方法主要就是来绑定不同标签的不同handler类的
nodeHandlerMap.put("if", new IfHandler());
}
public SqlSource parseScriptNode() {
MixedSqlNode rootSqlNode = parseDynamicTags(context);
SqlSource sqlSource;
if (isDynamic) {
sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
} else {
sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
}
return sqlSource;
}
}
在这里我们关心这个parseScriptNode方法,可以看到通过另一个解析动态标签的parseDynamicTags来得到一个rootSqlNode。那么解析完这里面的sql语句与标签,它是不是Dynamic就知道了,所以在这里就会分开了DynamicSqlSource和RawSqlSource。但是还是没有出现StaticSqlSource,别急,我们接着往下分析。
4.我们分别来看一下DynamicSqlSource和RawSqlSource的构造方法,见代码:
public DynamicSqlSource(Configuration configuration, SqlNode rootSqlNode) {
this.configuration = configuration;
this.rootSqlNode = rootSqlNode;
}
可以看到DynamicSqlSource的构造方法没有特别的,就是给了参数值。
再来看一下RawSqlSource的构造方法:
public RawSqlSource(Configuration configuration, SqlNode rootSqlNode, Class<?> parameterType) {
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;
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();
}
我们调用的是第一个构造方法,可以看到它的执行过程还是比较复杂的。其中通过getsql方法我们看到它调用了rootSqlNode的apply方法,这就是解析过程(验证了2.2的说法),所以说它的解析时机是在开始时。
那么在这里还用到了另一个类叫做SqlSourceBuilder来生成SqlSource,我们具体来看一下。
5.SqlSourceBuilder类的情况:
public class SqlSourceBuilder extends BaseBuilder {
public SqlSource parse(String originalSql, Class<?> parameterType, Map<String, Object> additionalParameters) {
ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType, additionalParameters);
GenericTokenParser parser = new GenericTokenParser("#{", "}", handler);
String sql = parser.parse(originalSql);
return new StaticSqlSource(configuration, sql, handler.getParameterMappings());
}
private static class ParameterMappingTokenHandler extends BaseBuilder implements TokenHandler{
//省略了所有代码,这个类主要的作用就是将特定的字符进行替换,如上文中将#{}替换成了占位符?,并且取出括号中的信息
}
}
首先来说它也是继承自BaseBuilder。我们用的也是最关心的parse方法会通过一个静态内部类来执行一系列操作,最终返回的是一个StaticSqlSource!
看到这里我们上面稍微串一下就了解:RawSqlSource中的属性SqlSource其实就是一个StaticSqlSource,那么又回到了2.3的这个问题,我们用RawSqlSource持有一个SqlSource干什么,或者具体地说,持有一个StaticSqlSource干什么?回答这个问题,我们需要分析一下StaticSqlSource类。
四、StaticSqlSource类的分析
1.由于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) {
return new BoundSql(configuration, sql, parameterMappings, parameterObject);
}
}
我们可以看到这个类直接就持有了String的sql,并且配合有一个List<ParameterMapping>的ParameterMappings,它们是干什么的呢?我举个例子解释一下,首先sql是一个
select * from table_name where id=?
的字符串,那么ParameterMappings用一个List结构来存储里面占位符的信息,比方说之前的表达式"#{id}"里面的字段值"id",type等等,那么这样配合这就可以构成完整的sql语句了。而且通过List来保证了顺序。
2.那么这个时候我们已经知道了RawSqlSource就是持有的StaticSqlSource了,那么反过来说,如果StaticSqlSource只有给RawSqlSource来用这一个功能的话为什么不直接把功能放在RawSqlSource中呢?这样的话还可以减少一个类,是Mybatis程序员的失误吗?
其实不是的,实际上StaticSqlSource还有其他地方用到了,我们来看一下DynamicSqlSource的部分代码:
public BoundSql getBoundSql(Object parameterObject) {
DynamicContext context = new DynamicContext(configuration, parameterObject);
rootSqlNode.apply(context);
//下面这段代码是不是很熟悉?不熟悉可以再看一下3.4 RawSqlSource的构造方法
SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass();
SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());
BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
context.getBindings().forEach(boundSql::setAdditionalParameter);
return boundSql;
}
应该看出来了吧,DynamicSqlSource再调用getBoundSql的过程中与RawSqlSource的构造方法中用到了重复的这段代码,也就是说明StaticSqlSource是DynamicSqlSource和RawSqlSource解析为BoundSql的一个中间环节,而且是必经的环节。只不过是时机不同,RawSqlSource只需要一直持有这个解析好的StaticSqlSource就行了,用的时候就调用StaticSqlSource的getBoundSql方法。而DynamicSqlSource必须每次使用时都转化一遍StaticSqlSource,然后再调用StaticSqlSource的getBoundSql方法,这样就会增加开销。
五、补充:BoundSql类
上面我们主观地可以推论BoundSql类其实就是封装的组织好的SQL语句,那么到底是不是这样呢?我们来看一下作者给的注释:
**
* An actual SQL String got from an {@link SqlSource} after having processed any dynamic content.
* The SQL may have SQL placeholders "?" and an list (ordered) of an parameter mappings
* with the additional information for each parameter (at least the property name of the input object to read
* the value from).
* <p>
* Can also have additional parameters that are created by the dynamic language (for loops, bind...).
*/
从这里可以看出来BoundSql类实际上就是持有一个真正的SQL,与我们推论的一致。这个SQL是从SqlSource中解析所有动态内容之后得到的,符合我们上面(三、四)的分析。这个持有的SQL中可能含有占位符?和一个有序的parameter mapping列表,类似于我们分析的StaticSqlSource的属性,看一下BoundSql的属性和构造方法就知道,其实不止是类似,而是完全一样的。
除此之外还补充了一句,BoundSql中也可能会有由动态SQL锁产生的其他的附加参数。