Mybatis’ handling of parameters is worth careful consideration, otherwise a series of errors will occur in the process of using directly confused.

In the past, WHEN I encountered a parameter binding error, I simply annotated @param and solved it, but then I ran into some problems that disproved my assumption that @param was not necessary for a single parameter. This raises a question, how exactly does Mybatis handle parameters?

Several common scenarios:

  • A single parameter
    • No annotations, based on references to ${} and #{}, both primitive types and custom objects work
    • Without annotations, list and array are not allowed due to the use of foreach tags
    • Without annotations, base type Boolean also fails based on the if tag

A preliminary packaging

The first processing is in MapperMethod:

private Object getParam(Object[] args) { final int paramCount = paramPositions.size(); if (args == null || paramCount == 0) { return null; } else if (! hasNamedParameters && paramCount == 1) { return args[paramPositions.get(0)]; } else { Map<String, Object> param = new MapperParamMap<Object>(); for (int i = 0; i < paramCount; i++) { param.put(paramNames.get(i), args[paramPositions.get(i)]); } // issue #71, add param names as param1, param2... but ensure backward compatibility for (int i = 0; i < paramCount; i++) { String genericParamName = "param" + String.valueOf(i + 1); if (! param.containsKey(genericParamName)) { param.put(genericParamName, args[paramPositions.get(i)]); } } return param; }}Copy the code

There are three possibilities: null, object[], and MapperParamMap. The third one constructs the common param1, parm2…

AuthAdminUser findAuthAdminUserByUserId (@ Param (” userId “) String userId);

When we define this in the Mapper interface, we follow the else code block above, and the MapperParamMap will contain two elements, one key with userId and the other param1.

The second processing is to wrap the arguments as a collection when calling executor’s Query method in DefaultSqlSession:

private Object wrapCollection(final Object object) { if (object instanceof List) { StrictMap<Object> map = new StrictMap<Object>(); map.put("list", object); return map; } else if (object ! = null && object.getClass().isArray()) { StrictMap<Object> map = new StrictMap<Object>(); map.put("array", object); return map; } return object; }Copy the code

Both MapperParamMap and StrictMap inherit from HashMap, except that an exception is thrown when super.containsKey(key) is set to false.

Present instance

When we write Mapper interfaces, we don’t usually use @param annotations for a parameter either.

What if the argument is of type List?

List<String> selectFeeItemTypeNameByIds(List<Integer> itemIds);Copy the code

Mapper configuration file:

<select id="selectFeeItemTypeNameByIds" parameterType="java.util.List" resultType="java.lang.String">
    SELECT fee_item_type_name
    FROM tb_uhome_fee_item_type
    WHERE fee_item_type_id IN
    <foreach collection="itemIds" item="itemId" open="(" close=")" separator="," >
        #{itemId}
    </foreach>
</select>Copy the code

Test it and report an error:

nested exception is org.apache.ibatis.binding.BindingException: Parameter ‘itemIds’ not found. Available parameters are [list]

Then replace itemIds with list:

<foreach collection="list" item="itemId" open="(" close=")" separator="," >
    #{itemId}
</foreach>Copy the code

This validates the operation in the source code above, in the wrapCollection method of DefaultSqlSession:

if (object instanceof List) {
  StrictMap<Object> map = new StrictMap<Object>();
  map.put("list", object);
  return map;
}Copy the code

What if this parameter is used in the if tag?

List<Map<String, Object>> selectPayMethodListByPlatform(boolean excludeInner);Copy the code

This is used in XML:

<select id="selectPayMethodListByPlatform" resultType="java.util.HashMap" parameterType="boolean">
    select a.`NAME`as payMethodName, a.`VALUE` as payMethod
    from tb_fcs_dictionary a
    where a.`CODE` = 'PAY_METHOD'
    and a.`STATUS` = 1
    and a.TYPE = 'PLATFORM'
    <if test="excludeInner">
        and a.value not in (14,98)
    </if>
</select>Copy the code

Error:

There is no getter for property named ‘excludeInner’ in ‘class java.lang.Boolean’

ContextAccessor: getProperty (ContextAccessor);

Let’s add a note @param (” excludeInner “) :

Instead of using annotations, a Boolean value is stored and returns NULL. With annotations, this value has a name and is stored in the MapperParamMap, which is directly available by name.

View the call stack

In ForEachSqlNode, the evaluateIterable method of ExpressionEvaluator is called to get the iterator object:

public Iterable<? > evaluateIterable(String expression, Object parameterObject) { try { Object value = OgnlCache.getValue(expression, parameterObject); if (value == null) throw new SqlMapperException("The expression '" + expression + "' evaluated to a null value."); if (value instanceof Iterable) return (Iterable<? >) value; if (value.getClass().isArray()) { // the array may be primitive, so Arrays.asList() may throw // a ClassCastException (issue 209). Do the work manually // Curse primitives! :) (JGB) int size = Array.getLength(value); List<Object> answer = new ArrayList<Object>(); for (int i = 0; i < size; i++) { Object o = Array.get(value, i); answer.add(o); } return answer; } throw new BuilderException("Error evaluating expression '" + expression + "'. Return value (" + value + ") was not iterable."); } catch (OgnlException e) { throw new BuilderException("Error evaluating expression '" + expression + "'. Cause: " + e, e); }}Copy the code

The evaluateBoolean method of ExpressionEvaluator is also called in IfSqlNode to check if the expression is correct:

public boolean evaluateBoolean(String expression, Object parameterObject) { try { 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; } catch (OgnlException e) { throw new BuilderException("Error evaluating expression '" + expression + "'. Cause: " + e, e); }}Copy the code

Both use Ognl to get the value of the expression:

Object value = OgnlCache.getValue(expression, parameterObject);

The actual processing

In the getBoundSql method of DynamicSqlSource:

  • Parameter binding


    DynamicContext context = new DynamicContext(configuration, parameterObject);

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()); }Copy the code
  • Node level by level processing (various tags and ${} processing)

rootSqlNode.apply(context);

This is the key to handling dynamic SQL by stripping out if, Choose, and foreach and using OGNL expressions to get the values of related attributes, such as the foreach and IF tags mentioned above.

It is then converted to simple text, and ${param} is finally processed in TextSqlNode, replacing it with the actual parameter value.

The replacement is as follows:

public String handleToken(String content) { try { Object parameter = context.getBindings().get("_parameter"); if (parameter == null) { context.getBindings().put("value", null); } else if (SimpleTypeRegistry.isSimpleType(parameter.getClass())) { context.getBindings().put("value", parameter); } Object value = OgnlCache.getValue(content, context.getBindings()); return (value == null ? "" : String.valueOf(value)); // issue #274 return "" instead of "null" } catch (OgnlException e) { throw new BuilderException("Error evaluating expression '" + content + "'. Cause: " + e, e); }}Copy the code
  • Parameter parsing (#{} handling)

SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType);

SqlSourceBuilder#parse:

public SqlSource parse(String originalSql, Class<? > parameterType) { ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType); GenericTokenParser parser = new GenericTokenParser("#{", "}", handler); String sql = parser.parse(originalSql); return new StaticSqlSource(configuration, sql, handler.getParameterMappings()); }Copy the code

The parse method of GenericTokenParser replaces #{xx} with? , the following SQL statement:

SELECT DISTINCT
    A.ORGAN_ID as organId,
    CONCAT(A. NAME, ' [', IFNULL(A.PY_NAME, ''), ']') as organName
FROM
    ORGAN A,
    ORGAN_REL B,
    V_USER_ORGAN C
WHERE
    A.ORGAN_ID = B.ORGAN_ID
AND B.ORGAN_CODE LIKE CONCAT(LEFT(C.ORGAN_CODE, 8), '%')
AND B.PAR_ID = 1
AND A.STATUS = 1
AND C.USER_ID = #{userId}Copy the code

After replacement:

SELECT DISTINCT
    A.ORGAN_ID as organId,
    CONCAT(A. NAME, ' [', IFNULL(A.PY_NAME, ''), ']') as organName
FROM
    ORGAN A,
    ORGAN_REL B,
    V_USER_ORGAN C
WHERE
    A.ORGAN_ID = B.ORGAN_ID
AND B.ORGAN_CODE LIKE CONCAT(LEFT(C.ORGAN_CODE, 8), '%')
AND B.PAR_ID = 1
AND A.STATUS = 1
AND C.USER_ID = ?Copy the code

Then construct a StaticSqlSource:

new StaticSqlSource(configuration, sql, handler.getParameterMappings());

This is just like using JDBC directly, using? As a placeholder.

Finally, we set the parameters in DefaultParameterHandler:

public void setParameters(PreparedStatement ps)
      throws SQLException {
    ErrorContext.instance().activity("setting parameters").object(mappedStatement.getParameterMap().getId());
    List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
    if (parameterMappings != null) {
      MetaObject metaObject = parameterObject == null ? null : configuration.newMetaObject(parameterObject);
      for (int i = 0; i < parameterMappings.size(); i++) {
        ParameterMapping parameterMapping = parameterMappings.get(i);
        if (parameterMapping.getMode() != ParameterMode.OUT) {
          Object value;
          String propertyName = parameterMapping.getProperty();
          PropertyTokenizer prop = new PropertyTokenizer(propertyName);
          if (parameterObject == null) {
            value = null;
          } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
            value = parameterObject;
          } else if (boundSql.hasAdditionalParameter(propertyName)) {
            value = boundSql.getAdditionalParameter(propertyName);
          } else if (propertyName.startsWith(ForEachSqlNode.ITEM_PREFIX)
              && boundSql.hasAdditionalParameter(prop.getName())) {
            value = boundSql.getAdditionalParameter(prop.getName());
            if (value != null) {
              value = configuration.newMetaObject(value).getValue(propertyName.substring(prop.getName().length()));
            }
          } else {
            value = metaObject == null ? null : metaObject.getValue(propertyName);
          }
          TypeHandler typeHandler = parameterMapping.getTypeHandler();
          if (typeHandler == null) {
            throw new ExecutorException("There was no TypeHandler found for parameter " + propertyName + " of statement " + mappedStatement.getId());
          }
          JdbcType jdbcType = parameterMapping.getJdbcType();
          if (value == null && jdbcType == null) jdbcType = configuration.getJdbcTypeForNull();
          typeHandler.setParameter(ps, i + 1, value, jdbcType);
        }
      }
    }
  }Copy the code

Here are five cases (the higher version incorporates the third and fourth) : – parameterObject is null and value is null. – parameterObject is of the type typeHandlerRegistry matches. Value is directly assigned to parameterObject. Through dynamic parameter values – parameters are dynamic and are in foreach (prefixed with FRCH), also through dynamic parameter values – complex objects or map types, through reflection

conclusion

Tags like if and foreach are evaluated directly through Ognl.

${} is processed in TextSqlNode using OGNL and replaced with the actual parameter value on the spot.

“#{}” is handled in SqlSourceBuilder parse using placeholders (?). The MetaObject value of Mybatis is used when setting the parameter.

When we use a single parameter without annotations: – used in tags of the form foreach and if (for the above two examples)

List<String> selectFeeItemTypeNameByIds(List<Integer> itemIds);

List<Map<String, Object>> selectPayMethodListByPlatform(boolean excludeInner);Copy the code

The getParam method of MapperMethod returns the two parameters themselves.

DefaultSqlSession’s wrapCollection method puts the list into a map with a key of “list”, Boolean returns itself.

Construct DynamicContext in the getBoundSql method of DynamicSqlSource

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()); }Copy the code

List will go else because it is wrapped. The Boolean type directly creates a ContextMap containing metaObject.

In any case, the “itemIds” are lost at this point and will not be retrieved later when parsing the expression.

The Boolean “excludeInner” will appear in ContextMap as follows (only with the value key being “_parameter”) :

key: "_parameter"  value: true
key: "_databaseId"  value: "MySQL"Copy the code

ParameterMetaObject, of type MetaObject, is not null.

Take a look at the rewritten GET method in ContextMap:

public Object get(Object key) { String strKey = (String) key; if (super.containsKey(strKey)) { return super.get(strKey); } if (parameterMetaObject ! = null) { Object object = parameterMetaObject.getValue(strKey); if (object ! = null) { super.put(strKey, object); } return object; } return null; }Copy the code

When there is none in the parent class (which there certainly is), it will go to parameterMetaObject to retrieve it. This raises the problem:

There is no getter for property named ‘excludeInner’ in ‘class java.lang.Boolean’

Go all the way to MetaObject’s getValue method and BeanWrapper’s get method, then treat it as a normal object and call its get method with reflection:

private Object getBeanProperty(PropertyTokenizer prop, Object object) { try { Invoker method = metaClass.getGetInvoker(prop.getName()); try { return method.invoke(object, NO_ARGUMENTS); } catch (Throwable t) { throw ExceptionUtil.unwrapThrowable(t); }} catch (RuntimeException e) {// This is a run-time exception. } catch (Throwable t) { throw new ReflectionException("Could not get property '" + prop.getName() + "' from " + object.getClass() + ". Cause: " + t.toString(), t); }}Copy the code

This excludeInner is a Boolean parameter, and there is no get method.

This completes the analysis of the two examples above, and gives you a general idea of how Mybatis handles parameters. In general, it’s okay to annotate @param, whether it’s a parameter or several! It’s going to give you all of them in the map, and then it’s going to fetch the whole map from CoxtMap, and since it’s a map, it’s going to fetch specific objects from the map.

From this we can see that if we declare the interface with a single map for all parameters, key for parameter name, value for parameter value, and no annotations, the effect is the same.


Have a question welcome to discuss, can leave a message also can add oneself QQ: 646653132

Detailed interpretation about the parameter bindings: blog.csdn.net/isea533/art…