I. 소개
프로젝트에서 입력 매개변수가 없을 때 추가 조건을 연결하는 쿼리 sql을 작성했는데 한참을 읽어보니 뭐가 잘못된 것인지 알 수 없었습니다. 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
}
spliced 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가 값이 없을 때 이 판단 조건이 추가된 이유는 무엇입니까?
<if test="exemptNo != null 및 exemptNo!=''"> 및 exempt_no = #{exemptNo} </if>
이 컬렉션을 순회할 때 블로거는 컬렉션 요소의 별칭을 exemptNo라고 명명했습니다. 그러면 mybatis는 별칭과 값을 공간 개체에 주입하고 다음 접합 판단에 사용할 것입니까?
<if test="exemptNoList != null 및 exemptNoList.size > 0"> 그리고 exempt_no <foreach collection="exemptNoList" item="exemptNo" open="("close=")" 구분 기호=","> #{exempt아니요} </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 메서드에 도달합니다.
exemptNo가 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 키-값 쌍은 매개변수가 두 개뿐인데, 컬렉션 순회 시 applyItem 메서드는 별칭을 키로 사용하고 매개변수 값은 ContextMap에 저장된다. mybatis에서 일시적으로 사용하기 위한 값이지만 사용 후 제거되지는 않습니다. .
여기에서 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;
}
}
문제는 명백합니다.컬렉션을 순회할 때 키-값 쌍 맵에 별칭이 저장됩니다.일회성이므로 실제로는 필요하지 않습니다.
4. 요약
mybatis의 이 문제는 sql의 별칭이 다른 매개 변수와 같을 수 없으며 일시적인 것이 아니며 sql에 다른 오류가 있으면 학생들도 블로거의 단계에 따라 디버깅할 수 있습니다.