Mybatis-3: Using <bind> tag causes other parameters to be empty(Null)

Created on 24 Dec 2019  ·  3Comments  ·  Source: mybatis/mybatis-3

~bug

mybatis version

3.4.0

java version

1.8.0_172

Database vendor and version

mysql 5.7.25 (Although this problem is not related to the database)

Test case or example project

请参阅具有完整回归测试的独立Maven项目
Please see self-contained maven project with the full regression test
Click here to my project in github

Steps to reproduce

当我使用bind标签后,我的其他非空参数会被转换成null值,见下案例
When using the tag, other non-null parameters I pass in will become Null.See below

mapping file

<select id="queryBug" resultType="java.util.HashMap">
        select
        *
        from t_blog
        <where>
            <bind name="params.a" value="params.a.replaceAll('-','')"/>
            id = #{params.id}
        </where>
</select>

test code

    @Test
    public void test()
    {
        BaseParams baseParams = new BaseParams();
        Map<String, String> map = new HashMap<String, String>();
        map.put("a","2019-12-24");
        map.put("id","2");
        baseParams.setParams(map);

        // Preparing: select * from t_blog WHERE id = ?
        // Parameters: 2(String)
        // Total: 1
        List query = baseDao.query(baseParams);

        // Preparing: select * from t_blog WHERE id = ?
        // Parameters: null
        // Total: 0
        List bug = baseDao.queryBug(baseParams);
    }

input log

Preparing: select * from t_blog WHERE id = ?
Parameters: null
Total: 0

Expected result

The bind tag should only modify the values I specified, and it turned my other values into null values
bind标签应该只能对我所指定的值进行修改,而它将我的其他值变成了空值

Actual result

Most helpful comment

Thank you for your answer
After reading the source code, I found that this is really not a bug.
Mainly reflected in the following code in DefaultParameterHandler class

@Override
  public void setParameters(PreparedStatement ps) {
    ErrorContext.instance().activity("setting parameters")
        .object(mappedStatement.getParameterMap().getId());
    List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
    if (parameterMappings != null) {
      for (int i = 0; i < parameterMappings.size(); i++) {
        ParameterMapping parameterMapping = parameterMappings.get(i);
        if (parameterMapping.getMode() != ParameterMode.OUT) {
          Object value;
          String propertyName = parameterMapping.getProperty();
          if (boundSql.hasAdditionalParameter(propertyName)) { // Since <bind> has created a new Map, It would enter here to take params <params.a>
            value = boundSql.getAdditionalParameter(propertyName);
          } else if (parameterObject == null) {
            value = null;
          } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
            value = parameterObject;
          } else { // Generally speaking, when no new map is created, the value of the map I pass in will be used.(here parameterObject)
            MetaObject metaObject = configuration.newMetaObject(parameterObject);
            value = metaObject.getValue(propertyName);
          }
          TypeHandler typeHandler = parameterMapping.getTypeHandler();
          JdbcType jdbcType = parameterMapping.getJdbcType();
          if (value == null && jdbcType == null) {
            jdbcType = configuration.getJdbcTypeForNull();
          }
          try {
            typeHandler.setParameter(ps, i + 1, value, jdbcType);
          } catch (TypeException e) {
            throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e);
          } catch (SQLException e) {
            throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e);
          }
        }
      }
    }
  }

Because here we will first determine whether params exists from additionalParameters, and if it exists, it will take the value in additionalParameters, But the Entry {id: 2} does not exist in additionalParameters, resulting in a null value.

Thanks again @harawata for the answer

It might be a little bit confusing, but I don't think this is a bug.

tl;dr
Don't use dot . in name of <bind />.
There is no point because <bind /> does not modify the parameter object passed to the statement. It always declares a new variable.
Just use a simple name. e.g.

<select id="queryBug" resultType="java.util.HashMap">
  select * from t_blog
  <where>
    <bind name="x" value="params.a.replaceAll('-','')"/>
    id = #{x}
  </where>
</select>

A slightly longer explanation:

If you write <bind name="params.a" value="123" />, it creates a new Map object with one entry {a=123} and put it under the name params in the _context_ (it's basically a map that contains parameters).
And variables declared by <bind /> have higher priority than the parameter object passed to the statement, so when referencing #{params.id} in the statement, MyBatis looks for a parameter with name 'params' and finds the newly created Map object which does not contain id.

My explanation may not be clear enough.
Please read the source code if you really want to know what is going on.

@mnesarco ,
Please comment if I am writing something inaccurate.

All 3 comments

It might be a little bit confusing, but I don't think this is a bug.

tl;dr
Don't use dot . in name of <bind />.
There is no point because <bind /> does not modify the parameter object passed to the statement. It always declares a new variable.
Just use a simple name. e.g.

<select id="queryBug" resultType="java.util.HashMap">
  select * from t_blog
  <where>
    <bind name="x" value="params.a.replaceAll('-','')"/>
    id = #{x}
  </where>
</select>

A slightly longer explanation:

If you write <bind name="params.a" value="123" />, it creates a new Map object with one entry {a=123} and put it under the name params in the _context_ (it's basically a map that contains parameters).
And variables declared by <bind /> have higher priority than the parameter object passed to the statement, so when referencing #{params.id} in the statement, MyBatis looks for a parameter with name 'params' and finds the newly created Map object which does not contain id.

My explanation may not be clear enough.
Please read the source code if you really want to know what is going on.

@mnesarco ,
Please comment if I am writing something inaccurate.

Thank you for your answer
After reading the source code, I found that this is really not a bug.
Mainly reflected in the following code in DefaultParameterHandler class

@Override
  public void setParameters(PreparedStatement ps) {
    ErrorContext.instance().activity("setting parameters")
        .object(mappedStatement.getParameterMap().getId());
    List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
    if (parameterMappings != null) {
      for (int i = 0; i < parameterMappings.size(); i++) {
        ParameterMapping parameterMapping = parameterMappings.get(i);
        if (parameterMapping.getMode() != ParameterMode.OUT) {
          Object value;
          String propertyName = parameterMapping.getProperty();
          if (boundSql.hasAdditionalParameter(propertyName)) { // Since <bind> has created a new Map, It would enter here to take params <params.a>
            value = boundSql.getAdditionalParameter(propertyName);
          } else if (parameterObject == null) {
            value = null;
          } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
            value = parameterObject;
          } else { // Generally speaking, when no new map is created, the value of the map I pass in will be used.(here parameterObject)
            MetaObject metaObject = configuration.newMetaObject(parameterObject);
            value = metaObject.getValue(propertyName);
          }
          TypeHandler typeHandler = parameterMapping.getTypeHandler();
          JdbcType jdbcType = parameterMapping.getJdbcType();
          if (value == null && jdbcType == null) {
            jdbcType = configuration.getJdbcTypeForNull();
          }
          try {
            typeHandler.setParameter(ps, i + 1, value, jdbcType);
          } catch (TypeException e) {
            throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e);
          } catch (SQLException e) {
            throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e);
          }
        }
      }
    }
  }

Because here we will first determine whether params exists from additionalParameters, and if it exists, it will take the value in additionalParameters, But the Entry {id: 2} does not exist in additionalParameters, resulting in a null value.

Thanks again @harawata for the answer

It might be a little bit confusing, but I don't think this is a bug.

tl;dr
Don't use dot . in name of <bind />.
There is no point because <bind /> does not modify the parameter object passed to the statement. It always declares a new variable.
Just use a simple name. e.g.

<select id="queryBug" resultType="java.util.HashMap">
  select * from t_blog
  <where>
    <bind name="x" value="params.a.replaceAll('-','')"/>
    id = #{x}
  </where>
</select>

A slightly longer explanation:

If you write <bind name="params.a" value="123" />, it creates a new Map object with one entry {a=123} and put it under the name params in the _context_ (it's basically a map that contains parameters).
And variables declared by <bind /> have higher priority than the parameter object passed to the statement, so when referencing #{params.id} in the statement, MyBatis looks for a parameter with name 'params' and finds the newly created Map object which does not contain id.

My explanation may not be clear enough.
Please read the source code if you really want to know what is going on.

@mnesarco ,
Please comment if I am writing something inaccurate.

Closing as it's not a bug.

Was this page helpful?
0 / 5 - 0 ratings