前言
在上一篇文章Mybaits Sql解析过程中,笔者介绍了Mybatis是如何根据注解生成动态Sql语句的。但是,在常用的查询中,除了生成语句之外,为Sql查询语句添加参数也是非常重要的一个环节。
本篇文章将会对Mybatis参数解析,设置的全过程,进行分析。简单点来说,就是我们会围绕着一下的方法调用例子,了解Mybatis如何将方法中的参数转化为Sql中的参数。
@Select({"<script>",
"SELECT account",
"FROM user",
"WHERE id IN",
"<foreach item='item' index='index' collection='list'",
"open='(' separator=',' close=')'>",
"#{item}",
"</foreach>",
"</script>"})
List<String> selectAccountsByIds(@Param("list") int[] ids);
Mybatis中的Sql参数设定与执行
首先,我们来通过一张简略的图片,来大概了解整个过程。
通过这张图,我们可以了解到几个重点。
通过MethodSignature将参数转化为Map
调用的具体方法如下
public Object getNamedParams(Object[] args) {
final int paramCount = names.size();
if (args == null || paramCount == 0) {
return null;
} else if (!hasParamAnnotation && paramCount == 1) {
return args[names.firstKey()];
} else {
final Map<String, Object> param = new ParamMap<Object>();
int i = 0;
for (Map.Entry<Integer, String> entry : names.entrySet()) {
//首先,根据@param中注解的值,将相关参数添加到Map中
param.put(entry.getValue(), args[entry.getKey()]);
// 然后,再将参数的通用名,也添加到Map中
final String genericParamName = GENERIC_NAME_PREFIX + String.valueOf(i + 1);
if (!names.containsValue(genericParamName)) {
param.put(genericParamName, args[entry.getKey()]);
}
i++;
}
return param;
}
}
通过以上的转换之后,我们在后续的函数调用中,就可以以键值对的形式来获取方法参数的值了。
DynamicSqlSource中的参数解析
在上一片文章中,我们提到过,SqlNode
中的apply
方法,会将我们定义在注解中的Sql,转化成带有占位符的Sql。而实际上,除了Sql的转化之外,它还会向DynamicContext
中添加Binding
(参数值与对应键的绑定)。
ForeachSqlNode
中的apply
方法
public boolean apply(DynamicContext context) {
Map<String, Object> bindings = context.getBindings();
//首先,通过解析器,将参数中的值解析成为一个可遍历的集合。
//初始状态下,bindins中只有_parameter -> { 'list' -> [1,2]}
//提取出来后的集合为[1,2]
final Iterable<?> iterable = evaluator.evaluateIterable(collectionExpression, bindings);
if (!iterable.iterator().hasNext()) {
return true;
}
boolean first = true;
applyOpen(context);
int i = 0;
for (Object o : iterable) {
DynamicContext oldContext = context;
if (first) {
context = new PrefixedContext(context, "");
} else if (separator != null) {
context = new PrefixedContext(context, separator);
} else {
context = new PrefixedContext(context, "");
}
int uniqueNumber = context.getUniqueNumber();
// Issue #709
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 {
//将对应的binding添加到DynamicContext中。
//第一次遍历时,增加到binding 为 { '__frch_index_0' -> 0 }
applyIndex(context, i, uniqueNumber);
//第一次遍历时,增加到binding 为 { '__frch_item_0' -> 1 }
applyItem(context, o, uniqueNumber);
}
contents.apply(new FilteredDynamicContext(configuration, context, index, item, uniqueNumber));
if (first) {
first = !((PrefixedContext) context).isPrefixApplied();
}
context = oldContext;
i++;
}
applyClose(context);
return true;
}
当SqlNode解析完成后,我们得到的DynamicContext中Sql如下:
SELECT account FROM user WHERE id IN ( #{__frch_item_0} , #{__frch_item_1} )
而在DynamicContext中的binding,则如下所示:
'__frch_index_0' -> 0,
'__frch_item_0' -> 1,
'__frch_index_1' -> 1,
'__frch_item_0' -> 2,
'_parameter' -> { 'list' -> [1,2] , 'param1' ->[1,2] },
'_databaseID' -> null
SqlSourceBuilder中获取ParameterMapping
虽然在DynamicContext中已经有了Bindings,但是Mybatis并不会直接使用这些binding进行查询。它会从含有占位符的语句中提取ParameterMapping关系,然后再根据ParameterMapping来对参数进行设置。
public SqlSource parse(String originalSql, Class<?> parameterType, Map<String, Object> additionalParameters) {
//ParameterMappingTokenHandler,在查找到特定的token之后,对token进行处理,并且返回处理后的字符串
ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType, additionalParameters);
//只负责找到被#{}包围的字符串,然后交由tokenHandler进行处理
GenericTokenParser parser = new GenericTokenParser("#{", "}", handler);
String sql = parser.parse(originalSql);
return new StaticSqlSource(configuration, sql, handler.getParameterMappings());
}
ParameterMappingTokenHandler
public String handleToken(String content) {
//首先,使用token中的标志符构造ParameterMapping,然后返回"?"
parameterMappings.add(buildParameterMapping(content));
return "?";
}
而在这一次处理之后,我们就能够能到可以被直接执行的Sql了。
SELECT account FROM user WHERE id IN ( ? , ? )
同时,也生成了对应的ParameterMapping(暂时忽略一些其它属性)
[
{ property: '__frch_item_0'},
{ property: '__frch_item_1'}
]
最后,在构造BoundSql时,Mybatis还会做如下的事情:
//将默认的参数,也作为BoundSql的默认参数
BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
//在context中生成的binding,则会做为额外的参数,也传给boundSql
for (Map.Entry<String, Object> entry : context.getBindings().entrySet()) {
boundSql.setAdditionalParameter(entry.getKey(), entry.getValue());
}
return boundSql;
参数的使用——DefaultParameterHandler
在这个类里面,我们只需要关注一个方法setParameters
。
public void setParameters(PreparedStatement ps) {
ErrorContext.instance().activity("setting parameters").object(mappedStatement.getParameterMap().getId());
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
if (parameterMappings != null) {
//parameterMapping是根据Sql中占位符逐个生成的,因此数组中的顺序也等同于sql中对应参数的顺序,直接进行遍历以及参数设置即可。
for (int i = 0; i < parameterMappings.size(); i++) {
ParameterMapping parameterMapping = parameterMappings.get(i);
if (parameterMapping.getMode() != ParameterMode.OUT) {
Object value;
String propertyName = parameterMapping.getProperty();
if (boundSql.hasAdditionalParameter(propertyName)) {
// 首先,从额外的参数中获取参数值,获取顺序很重要,这个与Mybatis中的issue #448有关
value = boundSql.getAdditionalParameter(propertyName);
} else if (parameterObject == null) {
value = null;
} else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
value = parameterObject;
} else {
//然后,再从parameterObject中进行获取。
MetaObject metaObject = configuration.newMetaObject(parameterObject);
value = metaObject.getValue(propertyName);
}
TypeHandler typeHandler = parameterMapping.getTypeHandler();
JdbcType jdbcType = parameterMapping.getJdbcType();
if (value == null && jdbcType == null) {
jdbcType = configuration.getJdbcTypeForNull();
}
try {
//考虑到Java类型与Mysql类型还有一个映射关系,所以使用typeHandler进行处理
typeHandler.setParameter(ps, i + 1, value, jdbcType);
} catch (TypeException e) {
throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e);
} catch (SQLException e) {
throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e);
}
}
}
}
}
到这里,参数的设定就已经完成了。
结语
在这片文章中,我们围绕着一个例子解释了Mybatis是如何进行参数设定的。在这里面,我们能够发现几个有意思的地方。
参数与ParameterMapping的分离。ParameterMapping根据解析后带占位符的Sql解析而得到,参数则始终保存在BoundSql的Parameter或者是AdditionalParameter中。这样的分离使得相互的职责更为明确,也更易于单元测试。
简单的设计。BoundSql作为执行的主体,里面只包含有Sqls,Parameter以及parameterMappings。