I. Introduction
In the project, I wrote a query sql that spliced an extra condition when there were no input parameters. After reading it for a while, I couldn’t find out what was wrong. A surprising guess emerged, whether the alias mybatis set in foreach was set in.
Two, investigation
The original code is as follows
select
<include refid="Base_Column_List"/>
from t_aac_shop_exempt_apply
where 1 = 1
<if test="exemptNoList != null and exemptNoList.size > 0">
and exempt_no in
<foreach collection="exemptNoList" item="exemptNo" open="(" close=")" separator=",">
#{exemptNo}
</foreach>
</if>
<if test="exemptNo != null and exemptNo!=''">
and exempt_no = #{exemptNo}
</if>
<if test="applyUserCode != null and applyUserCode!=''">
and apply_user_code = #{applyUserCode}
</if>
<if test="agentNo != null and agentNo!=''">
and agent_no = #{agentNo}
</if>
<if test="serviceAgentNoList != null and serviceAgentNoList.size > 0">
and agent_no in
<foreach collection="serviceAgentNoList" item="agentNo" open="(" close=")" separator=",">
#{agentNo}
</foreach>
</if>
<if test="compensateNo != null and compensateNo!=''">
and compensate_no = #{compensateNo}
</if>
<if test="month != null">
and month = #{month}
</if>
<if test="status != null">
and status = #{status}
</if>
<if test="statusList != null and statusList.size > 0">
and status in
<foreach collection="statusList" item="status" open="(" close=")" separator=",">
#{status}
</foreach>
</if>
<if test="startTime != null">
and gmt_create <![CDATA[ >= ]]> #{startTime}
</if>
<if test="endTime != null">
and #{endTime} <![CDATA[ >= ]]> gmt_create
</if>
and is_deleted = 0
The input parameter is
{
"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
}
The spliced sql result is like this
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
The question is, why is this judgment condition added when exemptNo has no value?
<if test="exemptNo != null and exemptNo!=''"> and exempt_no = #{exemptNo} </if>
When traversing this collection, the blogger named the alias of the collection element exemptNo, so will mybatis inject the alias and value into its space object, and use it in the next splicing judgment?
<if test="exemptNoList != null and exemptNoList.size > 0"> and exempt_no in <foreach collection="exemptNoList" item="exemptNo" open="("close=")" separator=","> #{exemptNo} </foreach> </if>
The blogger changed the alias, and the sql splicing is accurate
select
<include refid="Base_Column_List"/>
from t_aac_shop_exempt_apply
where 1 = 1
<if test="exemptNoList != null and exemptNoList.size > 0">
and exempt_no in
<foreach collection="exemptNoList" item="exempt" open="(" close=")" separator=",">
#{exempt}
</foreach>
</if>
<if test="exemptNo != null and exemptNo!=''">
and exempt_no = #{exemptNo}
</if>
<if test="applyUserCode != null and applyUserCode!=''">
and apply_user_code = #{applyUserCode}
</if>
<if test="agentNo != null and agentNo!=''">
and agent_no = #{agentNo}
</if>
<if test="serviceAgentNoList != null and serviceAgentNoList.size > 0">
and agent_no in
<foreach collection="serviceAgentNoList" item="agentNo" open="(" close=")" separator=",">
#{agentNo}
</foreach>
</if>
<if test="compensateNo != null and compensateNo!=''">
and compensate_no = #{compensateNo}
</if>
<if test="month != null">
and month = #{month}
</if>
<if test="status != null">
and status = #{status}
</if>
<if test="statusList != null and statusList.size > 0">
and status in
<foreach collection="statusList" item="st" open="(" close=")" separator=",">
#{st}
</foreach>
</if>
<if test="startTime != null">
and gmt_create <![CDATA[ >= ]]> #{startTime}
</if>
<if test="endTime != null">
and #{endTime} <![CDATA[ >= ]]> gmt_create
</if>
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
3. Principle
After knowing the reason, you can look at the source code of mybatis to see how it does it. This is actually a bug.
The breakpoint hits the getBoundSql method of DynamicSqlSource of org.apache.ibatis.scripting.xmltags
It can be seen that exemptNo is a null value, and this condition cannot be spliced.
Next look at rootSqlNode.apply(context), this step is the problem
@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<Object, OgnlClassResolver> context = Ognl.createDefaultContext(root, new OgnlClassResolver());
return Ognl.getValue(parseExpression(expression), context, root);
} catch (OgnlException e) {
throw new BuilderException("Error evaluating expression '" + expression + "'. Cause: " + e, e);
}
}
focus here
@Override
public boolean apply(DynamicContext context) {
Map<String, Object> 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<Object, Object> mapEntry = (Map.Entry<Object, Object>) o;
applyIndex(context, mapEntry.getKey(), uniqueNumber);
applyItem(context, mapEntry.getValue(), uniqueNumber);
} else {
applyIndex(context, i, uniqueNumber);
//将别名与参数值存入键值对
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;
}
At this time, the ContextMap key-value pair in the DynamicContext object used by mybatis has only two parameters. When traversing the collection, the applyItem method will use the alias as the key, and the parameter value will be stored in the ContextMap as the value for temporary use by mybatis, but it will not be removed after use. .
Here you can take a look at the general objects defined by mybatis. ContextMap inherits HashMap and is used to store sql parameters and sql key-value pairs after splicing by mybatis
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<String, Object> {
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;
}
}
The problem is obvious. When traversing the collection, the alias is stored in the key-value pair map. This is actually unnecessary, because it is a one-time use.
Four. Summary
This problem of mybatis tells us that the alias of sql cannot be the same as other parameters. It is not temporary. If there are other errors in sql, students can also follow the steps of the blogger to debug.