在写完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这个工具。
还是老规矩,先看SqlNode继承体系的UML类图。如下所示:
看到上面的UML。童鞋是不是觉得这些实现类跟我们平时写的动态sql里面的标签非常的相似,是的,在看过我们MyBatis初始化流程下的童鞋应该还记得,像if标签对应的就是我们的IfSqlNode,forEache标签对应的就是我们的ForEachSqlNode。下面我们来一一介绍每个实现类的功能以及创建,
在介绍实现类之前,我们先看一下SqlNode接口的定义,如下,为SqlNode的代码定义:
接口定义很简单,只有一个apply方法,apply方法的功能也只是解析动态sql标签的,至于怎么解析,我们在下面的实现类分析。
TestSqLNode表示的是映射文件里面sql语句的文本节点。它里面提供给了一个很重要的方法是isDynamic,代码定义如下:
代码分析:
注:text属性为创建TextNode对象是传进来的文本标签里面的文本。
TextSqlNod的apply方法实现代码如下,总的逻辑比较简单,主要是要对TokenParse这个接口要了解,童鞋可以先不关注DynamicContext这个接口是干啥的,只需要知道,他里面一定有我们在调用的时候传进来的实参。
代码分析:
IfSqlNode对应的是if标签,如下为IfSqlNode对应的代码定义:
apply方法代码分析:
注:对于if标签的子标签,通常来说,一般为textSqlNode。ifSqlNode标签的构造 其实在MyBatis初始化流程里面有讲到,童鞋可参考XMLScriptBuilder里面的内部类IfHandler构建ifSqlNode的代码。
TrimSqlNode对应的标签是Trim,TrimSqlNode会根据子节点的解析结果,相应的删除添加前缀或者后缀。TrimSqlNode相关属性的定义如下:
如下,为trimSqlNode的apply方法代码定义:
代码分析:
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相关代码。
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;
}
逻辑稍微有点绕,童鞋可结合代码分析,代码注释和测试用例一起分析:
代码分析:
测试用例:
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());
}
ChooseSqlNode对应于chose标签,类似Java语言标签的swtich功能,如下,为ChooseSqlNode的代码定义:
ChooseSqlNode的apply方法逻辑比较简单,首先遍历 ifSqlNodes 集合并调用其中SqlNode对象的 apply()方法,然后根据前面的处理结果决定是否调用 defaultSqlNode 的 apply()方法。此处逻辑,主要是要明白它对应的两个属性defaultSqlNode和ifSqlNodes表示的是什么意思,注释已在上面代码写明,至于这两个属性怎么解析出来的,可参开XMLScriptBuilder内部类ChooseHandler的handleNode方法。
VarDeclSqlNode对应bind标签。代码比较简单,实现方法就不再粘贴出来进行详细分析了。
SqlSource相关类的UML如下图所示:
对于DynamicSqlSource和RawSqlSource这两个类,童鞋在看完初始话流程下篇的时候应该看见过它的身影,在那里,曾介绍过它们,说RawSqlSource对应的是非静态sql,而DynamicSqlSource对应的是动态sql。这里,我们再集合上篇文章,一起分析SqlSource所有实现类的区别。
在分析各个实现类之前,我们先来看看SqlSource的代码定义:
SqlSource的定义比较简单,只有一个getBoundSql方法,该方法主要是完成动态sql语句解析的入口。
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;
}
代码分析:
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());
}
代码分析:
在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
StaticSqlSource在上面分析DynamicSqlSouce和RawSqlSource就已经提到,下面我们直接看它的代码定义:
代码没有什么多复杂,童鞋只需要知道各个属性的含义就可以,它的getBoundSql方法也比较简单,只是直接new一个BoundSql。
到这里,我们对SqlNode和SqlSource接口相关类已全部做了说明,SqlNode的apply方法主要是根据入参,解析动态sql,生成基本sql语句骨架,并且解析${}占位符的值,而SqlSouce的getBoundSql是根据SqlNode的解析后的结果,再次处理#{}占位符。本篇文章主要为了更好了解MyBatis初始化流程下篇做扩展的(文章连接),本篇有助于了解MyBatis是如何解析映射文件的。对于有些知识点的分析,博主可能总结分析不当之处,各位大佬可留下评论一起探讨探讨,感谢阅读。