Mybatis源码解析-BoundSql

mybatis 作为持久层,其操作数据库离不开 sql 语句。而 BoundSql 则是其保存 Sql 语句的对象

前提

  1. 针对 mybatis 的配置文件的节点解析,比如where/if/trim的节点解析可见文章Spring mybatis 源码篇章 -NodeHandler 实现类具体解析保存 Dynamic sql 节点信息

  2. 针对 mybatis 配置文件的解析帮助类 SqlSource[一般为 DynamicSqlSource] 的使用可见文章Spring mybatis 源码篇章 -XMLLanguageDriver 解析 sql 包装为 SqlSource

  3. 对 BoundSql 对象的调用获取可见文章Mybatis 源码分析 -BaseExecutor

本文将在上述的知识前提下展开对 Sql 语句的解析

BoundSql 的引用

主要是通过MappedStatement#getBoundSql()方法调用获取的。我们可以简单跟踪下其中的源码,如下

  public BoundSql getBoundSql(Object parameterObject) {
    // 通过 SqlSource 获取 BoundSql 对象
    BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
    // 校验当前的 sql 语句有无绑定 parameterMapping 属性
    List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
    if (parameterMappings == null || parameterMappings.isEmpty()) {
      boundSql = new BoundSql(configuration, boundSql.getSql(), parameterMap.getParameterMappings(), parameterObject);
    }
<span class="hljs-comment">// check for nested result maps in parameter mappings (issue #30)</span>
<span class="hljs-keyword">for</span> (ParameterMapping pm : boundSql.getParameterMappings()) {
  <span class="hljs-type">String</span> <span class="hljs-variable">rmId</span> <span class="hljs-operator">=</span> pm.getResultMapId();
  <span class="hljs-keyword">if</span> (rmId != <span class="hljs-literal">null</span>) {
    <span class="hljs-type">ResultMap</span> <span class="hljs-variable">rm</span> <span class="hljs-operator">=</span> configuration.getResultMap(rmId);
    <span class="hljs-keyword">if</span> (rm != <span class="hljs-literal">null</span>) {
      hasNestedResultMaps |= rm.hasNestedResultMaps();
    }
  }
}

<span class="hljs-keyword">return</span> boundSql;

}

RawSqlSource- 常用的 mybatis 解析 sql 帮助类

我们观察下其 getBoundSql() 方法,源码如下

  public BoundSql getBoundSql(Object parameterObject) {
    // 此处的 sqlSource 为 RawSqlSource 的内部属性
    return sqlSource.getBoundSql(parameterObject);
  }

我们看下 sqlSource 是如何生成的,由此观察其构造函数

public RawSqlSource(Configuration configuration, String sql, Class<?> parameterType) {
   // 通过 SqlSourceBuilder 来创建 sqlSource
    SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
    Class<?> clazz = parameterType == null ? Object.class : parameterType;
    sqlSource = sqlSourceParser.parse(sql, clazz, new HashMap<String, Object>());
  }

#{}的使用这里稍微提下,一般的写法都为{name,jdbcType=String,mode=out,javaType=java.lang.String...},其中 jdbcType 也可以不指定,系统会自动识别。上述的代码其实主要就是针对#{}字符内容的处理

注意:${}这样的字符是通过 DynamicSqlSource 来完成解析的,具体的解析读者可自行分析

我们可以继续看下 SqlSourceBuilder 类是如何解析获取 sql 语句的

SqlSourceBuilder#parse()

直接上源码

public SqlSource parse(String originalSql, Class<?> parameterType, Map<String, Object> additionalParameters) {
    // 对 #{} 这样的字符串内容的解析处理类
    ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType, additionalParameters);
    GenericTokenParser parser = new GenericTokenParser("#{", "}", handler);
    // 获取真实的可执行性的 sql 语句
    String sql = parser.parse(originalSql);
    // 包装成 StaticSqlSource 返回
    return new StaticSqlSource(configuration, sql, handler.getParameterMappings());
  }

简单的看下 ParameterMappingTokenHandler 是如何解析的,其是TokenHandler接口的实现类,我们就关注实现方法 handleToken

@Override
    public String handleToken(String content) {
      // 此处的作用就是对 `#{}` 节点中的 key 值保存映射,比如 javaType/jdbcType/mode 等信息,限于篇幅过长,读者可自行分析          
      parameterMappings.add(buildParameterMapping(content));
      // 将 `#{}` 替换为?,即一般包装成 `select * form test where name=? and age=?` 预表达式语句
      return "?";
    }

上述主要通过ParameterMappingTokenHandler类来完成对#{}字符串的解析,其中的映射信息则保存至 BoundSql 的 parameterMappings 属性中

总结

  1. BoundSql 语句的解析主要是通过对#{}字符的解析,将其替换成?。最后均包装成预表达式供PrepareStatement调用执行

  2. #{}中的 key 属性以及相应的参数映射,比如 javaType、jdbcType 等信息均保存至 BoundSql 的 parameterMappings 属性中供最后的预表达式对象PrepareStatement赋值使用