项目里写了一条查询sql在入参没有的情况下多拼接了一个条件,看了一会儿都感觉不出来哪里不对,一个惊讶的猜想浮现,在foreach设置的别名mybatis是不是给设置进去了。
二、排查原代码如下
select
from t_aac_shop_exempt_apply
where 1 = 1
and exempt_no in
#{exemptNo}
and exempt_no = #{exemptNo}
and apply_user_code = #{applyUserCode}
and agent_no = #{agentNo}
and agent_no in
#{agentNo}
and compensate_no = #{compensateNo}
and month = #{month}
and status = #{status}
and status in
#{status}
and gmt_create = ]]> #{startTime}
and #{endTime} = ]]> gmt_create
and is_deleted = 0
入参是
{
"model":
{
"exemptNoList":["MPSQ11739550014177280","MPSQ11750861482491904"],
"agentNo":"","compensateNo":"","applyUserCode":"",
"startTime":"2022-04-23 00:00:00",
"endTime":"2022-07-21 23:59:59"
},
"pageIndex":1,"pageSize":10,"queryCount":true,"start":0,"startPos":0
}
拼接的sql结果却是这样的
SELECT id, exempt_no, instance_code, agent_no, agent_name, compensate_no, MONTH, quantity, amount, reason, apply_user_name, apply_user_code, remark, act_status, STATUS, gmt_create, gmt_modify FROM t_aac_shop_exempt_apply WHERe 1 = 1 AND exempt_no IN (?, ?) AND exempt_no = ? AND gmt_create >= ? AND ? >= gmt_create AND is_deleted = 0 LIMIT 10
问题来了,为什么在exemptNo没有值的情况下,这个判断条件会被添加进去?
在这个集合遍历 的时候,博主把集合元素的别名命名为exemptNo,那么是不是mybatis会将别名和值注入到它的空间对象中,并且在下一个拼接判断使用了?
and exempt_no in #{exemptNo}
博主把别名换了一下,sql就拼接准确了
select
from t_aac_shop_exempt_apply
where 1 = 1
and exempt_no in
#{exempt}
and exempt_no = #{exemptNo}
and apply_user_code = #{applyUserCode}
and agent_no = #{agentNo}
and agent_no in
#{agentNo}
and compensate_no = #{compensateNo}
and month = #{month}
and status = #{status}
and status in
#{st}
and gmt_create = ]]> #{startTime}
and #{endTime} = ]]> gmt_create
and is_deleted = 0
SELECT id, exempt_no, instance_code, agent_no, agent_name, compensate_no, MONTH, quantity, amount, reason, apply_user_name, apply_user_code, remark, act_status, STATUS, gmt_create, gmt_modify FROM t_aac_shop_exempt_apply WHERe 1 = 1 AND exempt_no IN (?, ?) AND gmt_create >= ? AND ? >= gmt_create AND is_deleted = 0 LIMIT 10三、原理
知道了原因之后就可以看一下mybatis的源码了,看看他到底是怎么做的,这其实算是一个bug了。
断点打到org.apache.ibatis.scripting.xmltags的DynamicSqlSource的getBoundSql方法
可以看到exemptNo是空值,这个条件不可能被拼接。
接下来看rootSqlNode.apply(context),这一步是出问题的地方
@Override
public boolean apply(DynamicContext context) {
for (SqlNode sqlNode : contents) {
sqlNode.apply(context);
}
return true;
}
@Override
public boolean apply(DynamicContext context) {
判断是否拼接到sql
if (evaluator.evaluateBoolean(test, context.getBindings())) {
contents.apply(context);
return true;
}
return false;
}
public boolean evaluateBoolean(String expression, Object parameterObject) {
Object value = OgnlCache.getValue(expression, parameterObject);
if (value instanceof Boolean) {
return (Boolean) value;
}
if (value instanceof Number) {
return !new BigDecimal(String.valueOf(value)).equals(BigDecimal.ZERO);
}
return value != null;
}
public static Object getValue(String expression, Object root) {
try {
Map
重点在这里
@Override
public boolean apply(DynamicContext context) {
Map bindings = context.getBindings();
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
此时mybatis使用的DynamicContext对象中的ContextMap键值对只有两个参数,在集合遍历时applyItem方法会将别名作为键,参数值作为value存入ContextMap以供mybatis临时使用,但是用完之后没有被去掉。
这里可以看一下mybatis定义的通用对象,ContextMap继承了HashMap,用于存储sql参数以及mybatis拼接之后的sql键值对
public static final String PARAMETER_OBJECT_KEY = "_parameter";
public static final String DATABASE_ID_KEY = "_databaseId";
static {
OgnlRuntime.setPropertyAccessor(ContextMap.class, new ContextAccessor());
}
private final ContextMap bindings;
private final StringBuilder sqlBuilder = new StringBuilder();
private int uniqueNumber = 0;
public DynamicContext(Configuration configuration, Object parameterObject) {
if (parameterObject != null && !(parameterObject instanceof Map)) {
MetaObject metaObject = configuration.newMetaObject(parameterObject);
bindings = new ContextMap(metaObject);
} else {
bindings = new ContextMap(null);
}
bindings.put(PARAMETER_OBJECT_KEY, parameterObject);
bindings.put(DATABASE_ID_KEY, configuration.getDatabaseId());
}
static class ContextMap extends HashMap {
private static final long serialVersionUID = 2977601501966151582L;
private MetaObject parameterMetaObject;
public ContextMap(MetaObject parameterMetaObject) {
this.parameterMetaObject = parameterMetaObject;
}
@Override
public Object get(Object key) {
String strKey = (String) key;
if (super.containsKey(strKey)) {
return super.get(strKey);
}
if (parameterMetaObject != null) {
// issue #61 do not modify the context when reading
return parameterMetaObject.getValue(strKey);
}
return null;
}
}
问题很明显了,在集合遍历时将别名存入了键值对map,这其实是没有必要的,因为这是一次性使用的。
四、总结mybatis的这个问题告诉我们sql的别名不能与其他参数相同,他并不是临时性的,如果有sql的其他错误,同学们也可以跟着博主的步骤debug看看。



