一、SqlSource
在分析mapper映射文件解析时,我们只是整体介绍了mapper映射文件,而sql语句的解析过程没做进一步说明,分析中可知,用户编写的sql语句是存放在SqlSource对象中,但是语句中的参数怎么解析的、参数映射关系是怎么保存的以及动态sql是怎么实现的,将是本文的重点。
SqlSource是只有一个方法的简单接口。
public interface SqlSource {
BoundSql getBoundSql(Object parameterObject);
}
它有如下实现类:
从实现类看,它包含着动态的、静态的、默认的、注解类型等不同的实现方式,回到创建SqlSource入口位置:
//context是当前语句标签节点对象,parameterTypeClass是参数类型
//其中langDriver是XMLLanguageDriver的实例对象
SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
XMLLanguageDriver中的createSqlSource()方法:
public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) {
XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType);
return builder.parseScriptNode();
}
所以sql信息的解析是通过XMLScriptBuilder 对象实现,继续查看parseScriptNode()方法。
public SqlSource parseScriptNode() {
//得到一个混合类型的sql节点
MixedSqlNode rootSqlNode = parseDynamicTags(context);
SqlSource sqlSource;
if (isDynamic) {
//创建动态sql
sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
} else {
//创建原生类型的sql
sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
}
return sqlSource;
}
怎么判断是动态还是原生呢?我们需要从parseDynamicTags(context)方法中寻找答案。
protected MixedSqlNode parseDynamicTags(XNode node) {
List<SqlNode> contents = new ArrayList<>();
//先获取所有的子节点
NodeList children = node.getNode().getChildNodes();
for (int i = 0; i < children.getLength(); i++) {
XNode child = node.newXNode(children.item(i));
//如果当前子节点是个文本类型
if (child.getNode().getNodeType() == Node.CDATA_SECTION_NODE || child.getNode().getNodeType() == Node.TEXT_NODE) {
//拿到文本值
String data = child.getStringBody("");
//实例化一个文本对象
TextSqlNode textSqlNode = new TextSqlNode(data);
//判断是否是动态类型,判断逻辑是文本对象中是否有${}表达式
if (textSqlNode.isDynamic()) {
contents.add(textSqlNode);
isDynamic = true;
} else {
//不存在就封装成一个静态文本对象
contents.add(new StaticTextSqlNode(data));
}
//如果当前的节点是元素节点,也就是是一个标签节点,此时也是一种动态sql
} else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) { // issue #628
String nodeName = child.getNode().getNodeName();
//获取节点处理器,也就是子节点是trim、where、set、foreach、if、choose、when、otherwise、bind
NodeHandler handler = nodeHandlerMap.get(nodeName);
if (handler == null) {
throw new BuilderException("Unknown element <" + nodeName + "> in SQL statement.");
}
handler.handleNode(child, contents);
isDynamic = true;
}
}
return new MixedSqlNode(contents);
}
所以从分析来看,当语句中包含${}表达式、或者包含trim、where、set、foreach、if、choose、when、otherwise、bind子节点,那么当前的就是一个动态sql,也就是返回的对象是DynamicSqlSource,相反语句中只有#{}表达式,说明只是一个简单的原生类型sql。
二、BoundSql
第一部分把sql语句解析成一个DynamicSqlSource或RawSqlSource对象,此时都还是一个原生的文本类型数据,还不能直接使用,此时需要解析成直接可用的BoundSql对象。
BoundSql对象,从作者的注释来看,是从SqlSource处理完所有动态内容之后得到。我们先看看RawSqlSource实例是怎么处理的,查看其getBoundSql方法:
public BoundSql getBoundSql(Object parameterObject) {
return sqlSource.getBoundSql(parameterObject);
}
查看其构造方法可知,sqlSource是通过SqlSourceBuilder解析而来。
//其中sql又是通过getSql方法来获取
sqlSource = sqlSourceParser.parse(sql, clazz, new HashMap<>());
getSql():
private static String getSql(Configuration configuration, SqlNode rootSqlNode) {
DynamicContext context = new DynamicContext(configuration, null);
//此时会调用MixedSqlNode中的apply方法,该方法就是循环的执行contents中SqlNode的apply方法。
rootSqlNode.apply(context);
return context.getSql();
}
至于原生类型sql,我们只需要看StaticTextSqlNode中的apply方法。
@Override
public boolean apply(DynamicContext context) {
//把当前的语句添加到StringJoiner中(分隔符是空格)
context.appendSql(text);
return true;
}
也就是当前的静态sql语句会保存在StringJoiner中,继续回到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);
String sql;
//如果配置了shrinkWhitespacesInSql=true就会处理多余的空格先,默认是false
if (configuration.isShrinkWhitespacesInSql()) {
sql = parser.parse(removeExtraWhitespaces(originalSql));
} else {
//解析成可以执行的sql
sql = parser.parse(originalSql);
}
//返回一个静态的SqlSource实例
return new StaticSqlSource(configuration, sql, handler.getParameterMappings());
}
parse方法会把当前语句中#{}表达解析成"?",handler 会根据表达式中的内容,跟参数类型进行匹配,然后生成参数映射关系(List< ParameterMapping>)。具体可以查看handler的handleToken。
此时我们再看StaticSqlSource的getBoundSql方法。
public BoundSql getBoundSql(Object parameterObject) {
return new BoundSql(configuration, sql, parameterMappings, parameterObject);
}
会直接返回已经解析好的sql语句、参数映射关系、以及参数类型。
接下来看看DynamicSqlSource类型的处理过程。
public BoundSql getBoundSql(Object parameterObject) {
DynamicContext context = new DynamicContext(configuration, parameterObject);
//会根据不同的标签where、if、set等,分别进行解析
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);
//配置额外参数,如果传递的是单个参数,并且是基本类型,可以通过任意表达式获取到值,如果是多个参数,比如int id1,int id2 那么可以通过arg0、arg1或者param1、param2来获取(以此类推)
context.getBindings().forEach(boundSql::setAdditionalParameter);
return boundSql;
}
三、总结
本文简单分析了sql语句信息的解析过程,主要分析了动态和静态SqlSource,其实都是按照你的配置信息去一点点解析成对应的Java对象,最后反应到BoundSql 这个结果对象上。
注意:在写表达式的时候会有#{}和${},前者会对表达式内容用"?"代替,防止sql注入问题,后者会根据传递的值,进行直接赋值,这样会提升sql注入的风险,所以需尽量避免使用 ${}表达式,如果存在动态分组或者动态排序时则需使用 ${}方式
以上,有任何不对的地方,请指正,敬请谅解。