1:概述
在写完MyBaits核心处理层时,总感觉对于动态sql语句的解析没有写清楚,所以对于SqlNode,SqlSource这两个类相关的东西再写一篇博客,也算是对MyBatis源码的相关知识点的一次回顾,童鞋在看完这篇的时候,可以再回顾一下MyBatis初始化流程下,可能对sql语句解析的章节理解起来更容易。其实SqlNode和SqlSource这两个类,我个人感觉不仅仅是MyBaits动态sql的实质表现,更是让我们过渡,剥离XML相关内容的桥梁,就好比SQLNode,SqlSorce左边是XML,而右边纯粹是相关Java语言内容,非xml相关的东西。同时SqlNode和SqlSource对于后面的MyBatis执行流程的理解也是非常重要的。
在分析SqlNode相关知识之前,如果对Ognl不熟悉的同学,可参考下面这篇文章:https://www.cnblogs.com/cenyu/p/6233942.html 自行学习一下,因为SqlNode在解析一些表达式值的时候会用到Ongl这个工具。
2:SqlNode相关
还是老规矩,先看SqlNode继承体系的UML类图。如下所示:
看到上面的UML。童鞋是不是觉得这些实现类跟我们平时写的动态sql里面的标签非常的相似,是的,在看过我们MyBatis初始化流程下的童鞋应该还记得,像if标签对应的就是我们的IfSqlNode,forEache标签对应的就是我们的ForEachSqlNode。下面我们来一一介绍每个实现类的功能以及创建,
在介绍实现类之前,我们先看一下SqlNode接口的定义,如下,为SqlNode的代码定义:
接口定义很简单,只有一个apply方法,apply方法的功能也只是解析动态sql标签的,至于怎么解析,我们在下面的实现类分析。
2.1 TextSqlNode
TestSqLNode表示的是映射文件里面sql语句的文本节点。它里面提供给了一个很重要的方法是isDynamic,代码定义如下:
代码分析:
- 代码(1)处,创建TolenHandler实现类,该知识点在MyBatis的基础模块的解析模块提到过。
- 代码(2)处,创建TokenParse,指定openToken为${,closeToken为}。TokenParse的知识点也在基础模块的解析模块提到过。
- 代码(3)处,调用TokenParse的parse方法查找文本是否包含占位符,若有,则Dynamic置为true。
- 代码(4)处,根据代码(3)处的执行结果判断是否是动态Sql.
注:text属性为创建TextNode对象是传进来的文本标签里面的文本。
2.1.2 TextSqlNode#apply()方法
TextSqlNod的apply方法实现代码如下,总的逻辑比较简单,主要是要对TokenParse这个接口要了解,童鞋可以先不关注DynamicContext这个接口是干啥的,只需要知道,他里面一定有我们在调用的时候传进来的实参。
代码分析:
- 代码(1)处,创建TokenParse,指定openToken为“${”,closeToken为“}”。
- 代码(2)处,调用context里面的sqlBuilder方法将解析后的字符串添加到Context里面去。
2.2: IfSqlNode
IfSqlNode对应的是if标签,如下为IfSqlNode对应的代码定义:
apply方法代码分析:
- 代码(1)处。判断条件表达式是否满足。此处使用了Ognl表达式。
- 代码(2)处。调用if标签的子标签的apply方法。
注:对于if标签的子标签,通常来说,一般为textSqlNode。ifSqlNode标签的构造 其实在MyBatis初始化流程里面有讲到,童鞋可参考XMLScriptBuilder里面的内部类IfHandler构建ifSqlNode的代码。
2.3:TrimSqlNode
TrimSqlNode对应的标签是Trim,TrimSqlNode会根据子节点的解析结果,相应的删除添加前缀或者后缀。TrimSqlNode相关属性的定义如下:
如下,为trimSqlNode的apply方法代码定义:
代码分析:
- 代码(1)处,构建FilteredDynamicContext,该类是TrimSqlNode的内部类,实质也是DynamicContext的一个装饰器(装饰器模式在MyBatis里很常见,缓存模块就是典型的用法),FilteredDynamicContext仅仅对appendSql扩展了一下,代码不难,童鞋可以看一下。
- 代码(2)处,处理子标签的结果,因为SqlNode的构建形式本身就是一种树形结构,这个在MyBatis的初始化流程就谈到过。此处,童鞋可以简单假设一下,子标签就是if标签,对应的也就是调用IfSqlNode的apply方法。
- 代码(3)处,前后缀处理,这个就是处理FilteredDynamicContext的sqlBuffer的前缀或者后缀,然后调用context的appendSql将处理之后的结果添加进去。
不知道童鞋有没有思考过,此处,创建DynamicContext的装饰器,然后就扩展了appendSql方法,其余的都是调用delegate也就是context相关方法,个人感觉,这块MyBatis还是思考的非常周到的,因为通过创建这么一个装饰器类,让trim标签的前缀后缀的作用只能作用到trim的子标签,并不会污染到其它已经解析好的sql语句,因为在调用TrimSqlNode方法之前,可能已经调用了其它SqlNode的apply方法,context里面的sqlBuilder已经有值,如果没有这个装饰器的话,会污染已经解析出来的sql,这个小细节,童鞋可仔细揣摩揣摩。对于一些复杂的TrimSqlNode的构建童鞋可参考XMLScriptBuilder的内部类TrimHandler,这个也在MyBatis初始化流程下篇提到过。
2.4:WhereSqlNode 和SetSqlNode
WhereSqlNode 对应的标签是where标签,继承自TrimSqlNode,不同的是,WhereSqlNode的prefix属性固定为WHERE,prefixesToOverride为固定集合,如下:为WhereSqlNode代码定义:
public class WhereSqlNode extends TrimSqlNode {
private static List <String> prefixList = Arrays.asList("AND ", "OR ", "AND\n", "OR\n", "AND\r", "OR\r", "AND\t", "OR\t");
public WhereSqlNode(Configuration configuration, SqlNode contents) {
super(configuration, contents, "WHERE", prefixList, null, null);
}
}
SetSqlNode 对应的标签是set标签,同样也是继承TrimSqlNode,不同的是,SetSqlNode的prefix固定为 set ,prefixesToOverride,suffixesToOverride固定为逗号,如下,为对应的SetSqlNode的代码定义:
public class SetSqlNode extends TrimSqlNode {
private static final List<String> COMMA = Collections.singletonList(",");
public SetSqlNode(Configuration configuration,SqlNode contents) {
super(configuration, contents, "SET", COMMA, null, COMMA);
}
}
对于WhereSqlNode和SetSqlNode,其实就是TrimSqlNode的两种特殊表现形式,两者的apply方法就不再讲述了,逻辑同TrimSqlNode,童鞋们也可以结合自己平时写的动态sql,回顾一下,是不是其实它们的达到的功能是一样的。对于两者的构建,童鞋们可参考XMLScriptBuilder里面的内部类WhereHandler和SetHandler相关代码。
2.5 ForEachSqlNode
ForEachSqlNode对应的标签是foreach标签,在使用该标签迭代元素时,不仅可以使用集合的袁术和索引值,还可以在循环开始前,循环结束后添加指定的字符串,而且也允许在迭代过程中添加指定的分隔符。结合这些功能,我们先来看ForEachSqlNode定义了哪些属性,如下,为ForEachSqlNode的属性,含义我已经打上了注释,童鞋们可参考看一下:
在了解了ForEachSqlNode相关属性之后,我们再看看ForEachHandler是怎么构建ForEachSqlNode,这块其实在MyBatis初始化流程下篇的sql语句解析已经提到,如下为XMLScriptBuilder的内部类ForEachHandler,代码比较简单,童鞋可参开代码注释自己了解:
private class ForEachHandler implements NodeHandler {
public ForEachHandler() {
// Prevent Synthetic Access
}
@Override
public void handleNode(XNode nodeToHandle, List <SqlNode> targetContents) {
//获取foeEach标签的子节点
MixedSqlNode mixedSqlNode = parseDynamicTags(nodeToHandle);
//获取简单属性
String collection = nodeToHandle.getStringAttribute("collection");
String item = nodeToHandle.getStringAttribute("item");
String index = nodeToHandle.getStringAttribute("index");
String open = nodeToHandle.getStringAttribute("open");
String close = nodeToHandle.getStringAttribute("close");
String separator = nodeToHandle.getStringAttribute("separator");
//构建
ForEachSqlNode forEachSqlNode = new ForEachSqlNode(configuration, mixedSqlNode, collection, index, item, open, close, separator);
targetContents.add(forEachSqlNode);
}
}
了解了ForEachSqlNode的的内部属性和构建流程之后,我们再来看看它的apply方法,如下,为ForEachSqlNode的apply方法代码:
public boolean apply(DynamicContext context) {
Map <String, Object> bindings = context.getBindings();
//(1):通过ognl并结合实参获得collection表达式的的迭代值
final Iterable <?> iterable = evaluator.evaluateIterable(collectionExpression, bindings);
if (!iterable.iterator().hasNext()) {
return true;
}
boolean first = true;
//(2):应用Open属性
applyOpen(context);
int i = 0;
for (Object o : iterable) {
DynamicContext oldContext = context;
if (first || separator == null) {
context = new PrefixedContext(context, "");
} else {
context = new PrefixedContext(context, separator);
}
//解析到第几个子节点
int uniqueNumber = context.getUniqueNumber();
//(3):绑定本次循环参数
if (o instanceof Map.Entry) {
@SuppressWarnings("unchecked")
Map.Entry <Object, Object> mapEntry = (Map.Entry <Object, Object>) o;
applyIndex(context, mapEntry.getKey(), uniqueNumber);
applyItem(context, mapEntry.getValue(), uniqueNumber);
} else {
//设置参数值,params[index]=i,params[_frch_index_1..10]=i,
applyIndex(context, i, uniqueNumber);
//设置参数值,params[item] = o,params[_frch_item_1..10]=0
applyItem(context, o, uniqueNumber);
}
//(4):调用子节点的解析参数
contents.apply(new FilteredDynamicContext(configuration, context, index, item, uniqueNumber));
if (first) {
first = !((PrefixedContext) context).isPrefixApplied();
}
context = oldContext;
i++;
}
//(5):应用close方法
applyClose(context);
//(6):删除循环的参数
context.getBindings().remove(item);
context.getBindings().remove(index);
return true;
}
逻辑稍微有点绕,童鞋可结合代码分析,代码注释和测试用例一起分析:
代码分析:
- 代码(1)处,通过ognl并集合参数获得collection表达式的迭代变量。此处,需要了解ognl表达式,童鞋可集合本文开片推荐的博客进行学习。
- 代码(2)处,应用open属性,比较简单,就不再阐述。
- 代码(3)处,设置每次循环的参数,此处,会对参数类型进行判别,如果是集合类型,将新增四个参数,params[index]=i,params[_frch_index_1…10]=i,params[item]=o,params[_frch_item_1…10]=0,
- 代码(4)处,设置好每次循环的参数之后,调用子节点的apply方法。此处,FilteredDynamicContext需要特别留意appenSql方法,会对“#{}”占位符进行特殊处理,
测试用例:
private DynamicSqlSource createDynamicSqlSource(SqlNode... contents) throws IOException, SQLException {
//MyBatis配置文件
final String resource = "MapperConfig.xml";
final Reader reader = Resources.getResourceAsReader(resource);
SqlSessionFactory sqlMapper = new SqlSessionFactoryBuilder().build(reader);
Configuration configuration = sqlMapper.getConfiguration();
MixedSqlNode sqlNode = mixedContents(contents);
return new DynamicSqlSource(configuration, sqlNode);
}
@Test
void shouldSkipForEachWhenCollectionIsEmpty() throws Exception {
final HashMap <String, Integer[]> parameterObject = new HashMap <String, Integer[]>() {{
put("array", new Integer[]{1,2});
}};
final String expected = "SELECT * FROM BLOG";
DynamicSqlSource source = createDynamicSqlSource(new TextSqlNode("SELECT * FROM BLOG"),
new ForEachSqlNode(new Configuration(), mixedContents(
new TextSqlNode("#{item}")), "array", "index", "item", "WHERE id in (", ")", ","));
BoundSql boundSql = source.getBoundSql(parameterObject);
assertEquals(expected, boundSql.getSql());
assertEquals(0, boundSql.getParameterMappings().size());
}
第一次循环的调用中间参数:
结果:
2.6 ChooseSqlNode
ChooseSqlNode对应于chose标签,类似Java语言标签的swtich功能,如下,为ChooseSqlNode的代码定义:
ChooseSqlNode的apply方法逻辑比较简单,首先遍历 ifSqlNodes 集合并调用其中SqlNode对象的 apply()方法,然后根据前面的处理结果决定是否调用 defaultSqlNode 的 apply()方法。此处逻辑,主要是要明白它对应的两个属性defaultSqlNode和ifSqlNodes表示的是什么意思,注释已在上面代码写明,至于这两个属性怎么解析出来的,可参开XMLScriptBuilder内部类ChooseHandler的handleNode方法。
2.7 VarDeclSqlNode
VarDeclSqlNode对应bind标签。代码比较简单,实现方法就不再粘贴出来进行详细分析了。
3.sqlSource相关
SqlSource相关类的UML如下图所示:
对于DynamicSqlSource和RawSqlSource这两个类,童鞋在看完初始话流程下篇的时候应该看见过它的身影,在那里,曾介绍过它们,说RawSqlSource对应的是非静态sql,而DynamicSqlSource对应的是动态sql。这里,我们再集合上篇文章,一起分析SqlSource所有实现类的区别。
在分析各个实现类之前,我们先来看看SqlSource的代码定义:
SqlSource的定义比较简单,只有一个getBoundSql方法,该方法主要是完成动态sql语句解析的入口。
3.1 DynamicSqlSource
DynamicSqlSource是比较常用的SqlSource,实现逻辑比较简单,如下,为对应的getBoundSql代码实现:
@Override
public BoundSql getBoundSql(Object parameterObject) {
DynamicContext context = new DynamicContext(configuration, parameterObject);
//(1):解析动态sql
rootSqlNode.apply(context);
SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
//(2):获取参数类型
Class <?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass();
//(3):解析sql
SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());
//(4):调用StaticSqlSource的getBoundSql
BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
//(5):将context里面的参数赋值到boundSql里面去。
context.getBindings().forEach(boundSql::setAdditionalParameter);
return boundSql;
}
代码分析:
- 代码(1)处,解析动态sql,该处在前面已经说过,主要是调用sqlNode的子类实现动态sql的解析,通过该处,已得到了根据参数解析${}占位符的sql语句,基本sql骨架已完成,其实只剩下#{}占位符没有解析。
- 代码(2)处,获取实参类型。
- 代码(3)处,解析sql,该处需要了解SqlSourceBuilder这个类,主要是通过sqlSourceBuilder的parse方法完成解析的,参考3.1.2分析。此处返回的是StaticSqlSource这个类。
- 代码(4)处 调用StaticSqlSource返回BoundSql,StaticSqlSource的相关功能见3.3相关的分析。
- 代码(5)处,将context里面的参数赋值到boundSql里面去,代码比较简单,不再阐述,
3.1.2 SqlSourceBuilder
SqlSourceBuilder 主要完成了两方面的操作, 一方面是解析 SQL 语句中的“#{}”占位符 中定义的属性,格式类似于#{_frc_item_0, javaType= int, jdbcType=NUMERIC,typeHandler=MyTypeHandler},另一方面是将 SQL 语句中的#{}占位符替换成“?”,同样,SqlSouceBuilder也是继承于BaseBuilder,其核心方法parse代码定义如下:
public SqlSource parse(String originalSql, Class <?> parameterType, Map <String, Object> additionalParameters) {
//(1):构造token实现类
ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType, additionalParameters);
//(2):构造parse实现类
GenericTokenParser parser = new GenericTokenParser("#{", "}", handler);
//(3):解析
String sql = parser.parse(originalSql);
return new StaticSqlSource(configuration, sql, handler.getParameterMappings());
}
代码分析:
- 代码(1)处,构建Token实现类,到了这里,童鞋们应该对这个类很清楚了,这个解析类在我们的基础层解析器模块就已经提到过了
- 代码(2)处,同样,构建我们的parse类,这个类在解析器模块里也提到过,就不再阐述了。
- 代码(3)处,执行解析逻辑,对于ParameterMappingTokenHandler的handleToken方法,建议感兴趣的童鞋可以看一下,里面可能理解比较难。此处,需要特别注意的是,会生成ParameterMapping这个类的集合,ParameterMapping这个类比较简单,童鞋大概了解一下相关属性就行,总之,这个解析的地方完成了两个功能,一个是生成ParameterMapping这个,另外一个功能是将#{}替换成了?。
3.2 RawSqlSource
在MyBatis初始化下篇提到过,RawSqlSource是静态sql语句,童鞋们在看了MyBiats初始化下篇应该知道,MyBatis判断是否是动态sql语句的标准是文本是否有${}这个占位符。所以,此处我们可以想一下,RawSqlSource的getBoundSql方法的实现逻辑是否同DynamicSqlSouce的getBoundSql方法在执行完SqlNode的applyNode方法之后有相似之处。如下,为RawSqlSource的代码定义:
public class RawSqlSource implements SqlSource {
private final SqlSource sqlSource;
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;
//1:通过SqlSourceBuilder构建sqlSource,返回的是StaticSqlSource
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();
}
@Override
public BoundSql getBoundSql(Object parameterObject) {
return sqlSource.getBoundSql(parameterObject);
}
}
代码非常简单,在构造函数里通过调用SqlSourceBuilder这个类进行解析sql,该处逻辑通DynamicSqlSouce方法非常相似,在前面已经分析过,此处就再阐述了,对于Apply方法,也是调用StaticSqlSource的getBoundSql方法,详细分析见3.3
3.3 StaticSqlSource
StaticSqlSource在上面分析DynamicSqlSouce和RawSqlSource就已经提到,下面我们直接看它的代码定义:
代码没有什么多复杂,童鞋只需要知道各个属性的含义就可以,它的getBoundSql方法也比较简单,只是直接new一个BoundSql。
4总结
到这里,我们对SqlNode和SqlSource接口相关类已全部做了说明,SqlNode的apply方法主要是根据入参,解析动态sql,生成基本sql语句骨架,并且解析${}占位符的值,而SqlSouce的getBoundSql是根据SqlNode的解析后的结果,再次处理#{}占位符。本篇文章主要为了更好了解MyBatis初始化流程下篇做扩展的(文章连接),本篇有助于了解MyBatis是如何解析映射文件的。对于有些知识点的分析,博主可能总结分析不当之处,各位大佬可留下评论一起探讨探讨,感谢阅读。