1、概述:
解析SQL组件的作用如下:
(1)、将XML或Annotation中的SQL标签或注解内容解析成为SqlNode,SqlSource持有SqlNode引用。
(2)、用户在使用SQL操作数据库时,会调用SqlSource的getBuondSql()方法。该方法会使用DynamicContext和SqlNode的apply()方法来完成SQL语句的组装。
拼装SQL逻辑如下所示:
2、SqlSource和SqlNode
2.1、SqlSource
在Mybatis初始化过程中,映射配置文件中定义的SQL节点会被解析成MappedStatement对象,其中的SQL语句会被解析成SqlSource对象,SQL语句中定义的动态SQL节点、文本节点等则由SqlNode接口的相应实现表示。
SqlSource接口定义如下:
public interface SqlSource {
// 通过解析获取BoundSql
BoundSql getBoundSql(Object parameterObject);
}
SqlSource的子类如下所示:
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语句的解析。