I.はじめに
このプロジェクトでは、入力パラメータがない場合に追加の条件を結合するクエリ SQL を作成しましたが、しばらく読んでも何が間違っているのかわかりませんでした。別名 mybatis が設定されているかどうか、驚くべき推測が浮かび上がりました。 foreach に設定されました。
二、調査
元のコードは次のとおりです
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
入力パラメータは
{
"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
問題は、excludeNo に値がないのに、なぜこの判定条件を追加するのかということです。
<if test="exemptNo != null およびexcemptNo!=''"> およびexcempt_no = #{exemptNo} </if>
このコレクションを走査するときに、ブロガーはコレクション要素のエイリアスに「exciteNo」という名前を付けました。そのため、mybatis はエイリアスと値をスペース オブジェクトに挿入し、次のスプライシングの判断でそれを使用しますか?
<if test="exemptNoList != null andexcemptNoList.size > 0"> および <foreach collection="exemptNoList" item="exemptNo" open="("close=")" separator=","> # {免除いいえ} </foreach> </if>
ブロガーはエイリアスを変更しており、SQL スプライシングは正確です
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. 原則
理由がわかったら、mybatis のソース コードを見て、それがどのように機能するかを確認できますが、これは実際にはバグです。
ブレークポイントは、org.apache.ibatis.scripting.xmltags の DynamicSqlSource の getBoundSql メソッドにヒットします。
extendNo が null 値であり、この条件を結合できないことがわかります。
次に 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<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);
}
}
ここに焦点を当てます
@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;
}
現時点では、mybatis が使用する DynamicContext オブジェクトの ContextMap キーと値のペアには 2 つのパラメーターしかありません。コレクションを走査するとき、applyItem メソッドはエイリアスをキーとして使用し、パラメーター値は ContextMap にmybatis が一時的に使用するための値ですが、使用後に削除されません。
ここでは、mybatis によって定義された一般的なオブジェクトを確認できます。ContextMap は HashMap を継承し、mybatis によるスプライシング後の SQL パラメータと 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<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;
}
}
問題は明らかです。コレクションを走査するとき、エイリアスはキーと値のペアのマップに保存されます。これは 1 回限りの使用であるため、実際には不要です。
4. まとめ
mybatis のこの問題は、SQL のエイリアスを他のパラメータと同じにすることはできないことを示しています。これは一時的なものではありません。SQL に他のエラーがある場合、学生はブロガーの手順に従ってデバッグすることもできます。