Mybatis 스티칭 SQL 오류 및 소스코드 분석

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에 다른 오류가 있으면 학생들도 블로거의 단계에 따라 디버깅할 수 있습니다.

Supongo que te gusta

Origin blog.csdn.net/m0_69270256/article/details/125910931
Recomendado
Clasificación