Mybatis stitching sql error and source code analysis

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.

Guess you like

Origin blog.csdn.net/m0_69270256/article/details/125910931