mybatis源码分析:解析SQL组件

Scroll Down

1、概述:

解析SQL组件的作用如下:
(1)、将XML或Annotation中的SQL标签或注解内容解析成为SqlNode,SqlSource持有SqlNode引用。
(2)、用户在使用SQL操作数据库时,会调用SqlSource的getBuondSql()方法。该方法会使用DynamicContext和SqlNode的apply()方法来完成SQL语句的组装。
拼装SQL逻辑如下所示:
mybatissqllanguage.png

2、SqlSource和SqlNode

2.1、SqlSource

在Mybatis初始化过程中,映射配置文件中定义的SQL节点会被解析成MappedStatement对象,其中的SQL语句会被解析成SqlSource对象,SQL语句中定义的动态SQL节点、文本节点等则由SqlNode接口的相应实现表示。
SqlSource接口定义如下:

public interface SqlSource {

  // 通过解析获取BoundSql
  BoundSql getBoundSql(Object parameterObject);

}

SqlSource的子类如下所示:
image.png
SqlSource的子类,DynamicSqlSource负责处理动态SQL语句,RawSqlSource负责处理静态语句,两者最终都会将处理后的SQL语句封装成StaticSqlSource返回。DynamicSqlSource与StaticSqlSource的主要区别是:StaticSqlSource中记录的SQL语句中可能含有"?"占位符,但是可以直接交给数据库执行;DynamicSqlSource中封装的SQL语句还需要进行一系列的解析,才能最终形成数据库可执行的SQL语句。

2.2、SqlNode

SqlNode接口用来解析动态SQL节点
SqlNode接口的定义如下:

public interface SqlNode {
  // apply()是SqlNode接口中定义的唯一方法,该方法会根据用户传入的实参,参数解析该SqlNode所记录的动态SQL节点,
  // 并调用DynamicContext.appendSql()方法将解析后的SQL片段追加到DynamicContext.sqlBuilder中保存,
  // 当SQL节点下的所有SqlNode完成解析后,就可以直接从DynamicContext中获取一条动态生成的完整SQL语句。
  boolean apply(DynamicContext context);
}

SqlNode接口有多个实现类,每个实现类对应一个动态SQL节点。

2.2.1、StaticTextSqlNode和MixedSqlNode

StaticTextSqlNode中使用text字段(String类型)记录了对应的非动态SQL语句节点,其apply()方法直接将text字段追加到DynamicContext.sqlBuilder字段中。
MixedSqlNode中使用contents字段记录其子节点对应的SqlNode对象集合,其apply()方法会循环调用contents集合中所有SqlNode对象的apply方法。

2.2.2、TextSqlNode

TextSqlNode表示的是包含"${}"占位符的动态SQL节点。TextSqlNode.apply()方法会使用GenericTokenParser解析"${}"占位符,并直接替换成用户给定的实际参数值。

@Override
public boolean apply(DynamicContext context) {
    // 创建GenericTokenParser解析器。
    GenericTokenParser parser = createParser(new BindingTokenParser(context, injectionFilter));
    // 将解析后的SQL片段添加到DynamicContext中。
    context.appendSql(parser.parse(text));
    return true;
}

private GenericTokenParser createParser(TokenHandler handler) {
    // 解析的是"${}"占位符
    return new GenericTokenParser("${", "}", handler);
}

BindingTokenParser是TextSqlNode中定义的内部类,继承了TokenHandler接口,它的主要功能是根据DynamicContext.bindings集合中的信息解析SQL语句中的${}占位符。

@Override
public String handleToken(String content) {
  // 获取用户提供的实参
  Object parameter = context.getBindings().get("_parameter");
  if (parameter == null) {
    context.getBindings().put("value", null);
  } else if (SimpleTypeRegistry.isSimpleType(parameter.getClass())) {
    context.getBindings().put("value", parameter);
  }
  // 通过OGNL解析content的值
  Object value = OgnlCache.getValue(content, context.getBindings());
  String srtValue = (value == null ? "" : String.valueOf(value)); // issue #274 return "" instead of "null"
  // 检测合法性
  checkInjection(srtValue);
  return srtValue;
}
2.2.3、IfSqlNode

IfSqlNode对应的动态SQL节点是IF节点,其中定义的字段的含义如下:

// ExpressionEvaluator对象用于解析if节点的test表达式的值
private final ExpressionEvaluator evaluator;
// 记录了test表达式
private final String test;
// 记录了if节点的子节点
private final SqlNode contents;

@Override
public boolean apply(DynamicContext context) {
    //检测test的表达式如果为true,则执行子节点的apply()方法
    if (evaluator.evaluateBoolean(test, context.getBindings())) {
      contents.apply(context);
      return true;
    }
    return false;
}
2.2.4、TrimSqlNode

TrimSqlNode会根据子节点的解析结果,添加或删除相应的前缀或后缀。
TrimSqlNode的字段含义如下:

// trim标签下的所有子节点
private final SqlNode contents;
// 为trim节点包裹的SQL语句添加前缀
private final String prefix;
// 为trim节点包裹的SQL语句添加后缀
private final String suffix;
// 如果trim节点包裹的SQL语句是空语句,则删除指定的前缀。
private final List<String> prefixesToOverride;
// 如果trim节点包裹的SQL语句是空语句,则删除执行的后缀
private final List<String> suffixesToOverride;
private final Configuration configuration;

2.3、SqlSourceBuilder

在用户执行SQL语句之后,SQL语句会被传递到SqlSourceBuilder中进行进一步解析。SqlSourceBuilder主要完成了两方面操作,一方面是解析SQL语句中的#{}占位符中定义的属性,格式类似于#{_frc_item_0, javaType=int, jdbcType=NUMBER, typeHandler=MyTypeHandler},另一方面是将SQL语句中的#{}占位符替换成?占位符。
SqlSourceBuilder的核心逻辑位于parse()方法中:

@Override
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);
    for (Map.Entry<String, Object> entry : context.getBindings().entrySet()) {
      boundSql.setAdditionalParameter(entry.getKey(), entry.getValue());
    }
    return boundSql;
}

public SqlSource parse(String originalSql, Class<?> parameterType, Map<String, Object> additionalParameters) {
    // 创建ParameterMappingTokenHandler对象,它是解析"#{}"占位符中的参数以及替换占位符的核心
    ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType, additionalParameters);
    // 使用GenericTokenParser配合解析#占位符
    GenericTokenParser parser = new GenericTokenParser("#{", "}", handler);
    // 将#{}占位符替换成?SQL语句以及参数对应的ParameterMapping集合
    String sql = parser.parse(originalSql);
    return new StaticSqlSource(configuration, sql, handler.getParameterMappings());
}

总结:
无论是StaticSqlSource、DynamicSqlSource还是RawSqlSource最终都会统一生成BoundSql对象,其中封装了完整的SQL语句、参数映射关系、以及用户传入的参数。另外DynamicSqlSource负责处理动态SQL语句,RawSqlSource负责处理静态SQL语句,除此之外,两者解析SQL的时机也不一样,前者解析时机是在实际执行SQL语句之前,而后者则是在Mybatis初始化时完成SQL语句的解析。