Mybatis源码阅读笔记2-探究SqlSource

mybatis 3.5.5的版本

在上一篇xml解析过程完成之后我们知道最重要的具体的sql信息放到了SqlSource中,那么今天就来探究SqlSource接口。

xml文件解析过程: https://blog.csdn.net/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锁产生的其他的附加参数。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

zxzfcsu

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值