The same variable name in mybatis #foreach causes the value override problem to be solved

Time:2021-10-22

background

Use mybatis to execute the following query:

unit testing


@Test
public void test1() {
    String resource = "mybatis-config.xml";
    InputStream inputStream = null;
    try {
        inputStream = Resources.getResourceAsStream(resource);
    } catch (IOException e) {
        e.printStackTrace();
    }
    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
    try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
        CommonMapper mapper = sqlSession.getMapper(CommonMapper.class);
        QueryCondition queryCondition = new QueryCondition();
        List<Integer> list = new ArrayList<>();
        list.add(1);
        list.add(2);
        list.add(3);
        queryCondition.setWidthList(list);
        System.out.println(mapper.findByCondition(queryCondition));
    }
}

XML


<select parameterType="cn.liupjie.pojo.QueryCondition" resultType="cn.liupjie.pojo.Test">
    select * from test
    <where>
        <if test="id != null">
            and id = #{id,jdbcType=INTEGER}
        </if>
        <if test="widthList != null and widthList.size > 0">
            <foreach collection="widthList" open="and width in (" close=")" item="width" separator=",">
                #{width,jdbcType=INTEGER}
            </foreach>
        </if>
        <if test="width != null">
            and width = #{width,jdbcType=INTEGER}
        </if>
    </where>
</select>

SQL to print:
DEBUG [main] – ==>  Preparing: select * from test WHERE width in ( ? , ? , ? ) and width = ?
DEBUG [main] – ==> Parameters: 1(Integer), 2(Integer), 3(Integer), 3(Integer)

Mybatis version


<dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis</artifactId>
    <version>3.4.1</version>
</dependency>

This is an old project of the company. This problem is encountered in the process of iteration. This is a record!
PS: this bug has been fixed in mybatis-3.4.5. Mybatis maintainers also recommend not to use duplicate variable names in item / index.

Cause of problem (abbreviated version)

  • After the defaultsqlsession is obtained, the proxy class of mapper interface will be obtained and the query will be executed by calling the method of the proxy class
  • Before actually executing the database query, you need to splice the executable SQL. This operation is performed in the dynamicsqlsource#getboundsql method
  • When the foreach tag is parsed, a mapping between the item attribute value and the variable value (for example, width: 1) will be cached in each loop. After the foreach tag is parsed, one (width: 3) will be retained in the cached parameter mapping relationship
  • When parsing to the last if tag, because the width variable has a value, the if is judged to be true, and splicing is performed normally, resulting in an error
  • In version 3.4.5, after the foreach tag parsing is completed, two lines of code are added to solve this problem.
//After parsing the foreach tag, remove the item from the bindings
  context.getBindings().remove(item);
  context.getBindings().remove(index);

Mybatis process source code analysis (long text warning, self access on demand)

1、 Get sqlsessionfactory

Entrance, follow the build method

//Obtain sqlsessionfactory. After parsing, encapsulate the content in XML into a configuration object,
//Use this object to construct a defaultsqlsessionfactory object and return
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

Go to the sqlsessionfactorybuilder #build method

public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
  try {
    //Get xmlconfigbuilder. In the construction method of xmlconfigbuilder, an xpathparser object will be created
    //When you create an xpathparser object, the mybatis-config.xml file is converted to a document object
    XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
    //Call the xmlconfigbuilder#parse method to start parsing the configuration file of mybatis
    return build(parser.parse());
  } catch (Exception e) {
    throw ExceptionFactory.wrapException("Error building SqlSession.", e);
  } finally {
    ErrorContext.instance().reset();
    try {
      inputStream.close();
    } catch (IOException e) {
      // Intentionally ignore. Prefer previous error.
    }
  }
}

Follow the parse method to the xmlconfigbuilder#parseconfiguration method

private void parseConfiguration(XNode root) {
  try {
    Properties settings = settingsAsPropertiess(root.evalNode("settings"));
    //issue #117 read properties first
    propertiesElement(root.evalNode("properties"));
    loadCustomVfs(settings);
    typeAliasesElement(root.evalNode("typeAliases"));
    pluginElement(root.evalNode("plugins"));
    objectFactoryElement(root.evalNode("objectFactory"));
    objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
    reflectorFactoryElement(root.evalNode("reflectorFactory"));
    settingsElement(settings);
    // read it after objectFactory and objectWrapperFactory issue #631
    environmentsElement(root.evalNode("environments"));
    databaseIdProviderElement(root.evalNode("databaseIdProvider"));
    typeHandlerElement(root.evalNode("typeHandlers"));
    //Mapper is parsed here
    mapperElement(root.evalNode("mappers"));
  } catch (Exception e) {
    throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
  }
}

Come to mapperelement method

//This mappers configuration: < mapper resource = "XML / commmapper. XML" / >
private void mapperElement(XNode parent) throws Exception {
  if (parent != null) {
    for (XNode child : parent.getChildren()) {
      if ("package".equals(child.getName())) {
        String mapperPackage = child.getStringAttribute("name");
        configuration.addMappers(mapperPackage);
      } else {
        String resource = child.getStringAttribute("resource");
        String url = child.getStringAttribute("url");
        String mapperClass = child.getStringAttribute("class");
        if (resource != null && url == null && mapperClass == null) {
          //So go here, read the XML file and start parsing
          ErrorContext.instance().resource(resource);
          InputStream inputStream = Resources.getResourceAsStream(resource);
          //Like the xmlconfigbuilder object created above, the XML file is also converted into a document object during internal construction
          XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
          //Analysis
          mapperParser.parse();
        } else if (resource == null && url != null && mapperClass == null) {
          ErrorContext.instance().resource(url);
          InputStream inputStream = Resources.getUrlAsStream(url);
          XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
          mapperParser.parse();
        } else if (resource == null && url == null && mapperClass != null) {
          Class<?> mapperInterface = Resources.classForName(mapperClass);
          configuration.addMapper(mapperInterface);
        } else {
          throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
        }
      }
    }
  }
}

The xmlmapperbuilder class is responsible for parsing the content in the XML where the SQL statement is located

//Parse method
public void parse() {
  if (!configuration.isResourceLoaded(resource)) {
    //Parsing mapper Tags
    configurationElement(parser.evalNode("/mapper"));
    configuration.addLoadedResource(resource);
    bindMapperForNamespace();
  }

  parsePendingResultMaps();
  parsePendingChacheRefs();
  parsePendingStatements();
}

//Configurationelement method
private void configurationElement(XNode context) {
  try {
    String namespace = context.getStringAttribute("namespace");
    if (namespace == null || namespace.equals("")) {
      throw new BuilderException("Mapper's namespace cannot be empty");
    }
    builderAssistant.setCurrentNamespace(namespace);
    cacheRefElement(context.evalNode("cache-ref"));
    cacheElement(context.evalNode("cache"));
    parameterMapElement(context.evalNodes("/mapper/parameterMap"));
    resultMapElements(context.evalNodes("/mapper/resultMap"));
    sqlElement(context.evalNodes("/mapper/sql"));
    //Parsing various types of SQL statements: select Insert update delete
    buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
  } catch (Exception e) {
    throw new BuilderException("Error parsing Mapper XML. Cause: " + e, e);
  }
}

private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
  for (XNode context : list) {
    //Create an xmlstatementbuilder object
    final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
    try {
      //Analysis
      statementParser.parseStatementNode();
    } catch (IncompleteElementException e) {
      configuration.addIncompleteStatement(statementParser);
    }
  }
}

The XML statementbuilder is responsible for parsing a single select, insert, update and delete node

public void parseStatementNode() {
  String id = context.getStringAttribute("id");
  String databaseId = context.getStringAttribute("databaseId");
  //Judge whether the databaseid matches, splice the namespace + '. + ID, and judge whether this ID already exists
  if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
    return;
  }

  Integer fetchSize = context.getIntAttribute("fetchSize");
  Integer timeout = context.getIntAttribute("timeout");
  String parameterMap = context.getStringAttribute("parameterMap");
  //Get parameter type
  String parameterType = context.getStringAttribute("parameterType");
  //Get parameter type的class对象
  Class<?> parameterTypeClass = resolveClass(parameterType);
  String resultMap = context.getStringAttribute("resultMap");
  String resultType = context.getStringAttribute("resultType");
  String lang = context.getStringAttribute("lang");
  LanguageDriver langDriver = getLanguageDriver(lang);
  //Gets the class object of resulttype
  Class<?> resultTypeClass = resolveClass(resultType);
  String resultSetType = context.getStringAttribute("resultSetType");
  StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
  ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);
  //Get the type of select Insert update delete
  String nodeName = context.getNode().getNodeName();
  SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
  boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
  boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
  boolean useCache = context.getBooleanAttribute("useCache", isSelect);
  boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);

  // Include Fragments before parsing
  XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
  includeParser.applyIncludes(context.getNode());

  // Parse selectKey after includes and remove them.
  processSelectKeyNodes(id, parameterTypeClass, langDriver);

  // Parse the SQL (pre: <selectKey> and <include> were parsed and removed)
  //Get the sqlsource object. Langdriver is the default xmlanguagedriver, which is set in new configuration
  //If the SQL contains element nodes or $, it returns dynamicsqlsource; otherwise, it returns rawsqlsource
  SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
  String resultSets = context.getStringAttribute("resultSets");
  String keyProperty = context.getStringAttribute("keyProperty");
  String keyColumn = context.getStringAttribute("keyColumn");
  KeyGenerator keyGenerator;
  String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;
  keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true);
  if (configuration.hasKeyGenerator(keyStatementId)) {
    keyGenerator = configuration.getKeyGenerator(keyStatementId);
  } else {
    keyGenerator = context.getBooleanAttribute("useGeneratedKeys",
        configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))
        ? new Jdbc3KeyGenerator() : new NoKeyGenerator();
  }

  builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
      fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
      resultSetTypeEnum, flushCache, useCache, resultOrdered,
      keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
}

2、 Get sqlsession

As can be seen from the above, the sqlsessionfactory here uses defaultsqlsessionfactory

private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
  Transaction tx = null;
  try {
    final Environment environment = configuration.getEnvironment();
    final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
    tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
    //Create an actuator. The default is simpleexecution
    //If caching is enabled in the configuration file (enabled by default), it is cacheingexecution
    final Executor executor = configuration.newExecutor(tx, execType);
    //Returns the defaultsqlsession object
    return new DefaultSqlSession(configuration, executor, autoCommit);
  } catch (Exception e) {
    closeTransaction(tx); // may have fetched a connection so lets call close()
    throw ExceptionFactory.wrapException("Error opening session.  Cause: " + e, e);
  } finally {
    ErrorContext.instance().reset();
  }
}

A defaultsqlsession object is obtained here

3、 Execute SQL

Get the object of CommonMapper, where CommonMapper is an interface, so it is a proxy object, and the proxy class is mapperproxy

Execute the query method to the invoke method of mapperproxy

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
  if (Object.class.equals(method.getDeclaringClass())) {
    try {
      return method.invoke(this, args);
    } catch (Throwable t) {
      throw ExceptionUtil.unwrapThrowable(t);
    }
  }
  //Cache
  final MapperMethod mapperMethod = cachedMapperMethod(method);
  //Execution: select Insert update delete
  return mapperMethod.execute(sqlSession, args);
}

When the operation is executed, the executeformany method is determined according to the select operation and the return value type (reflection method acquisition)


caseSELECT:
  if (method.returnsVoid() && method.hasResultHandler()) {
    executeWithResultHandler(sqlSession, args);
    result = null;
  } else if (method.returnsMany()) {
    result = executeForMany(sqlSession, args);
  } else if (method.returnsMap()) {
    result = executeForMap(sqlSession, args);
  } else if (method.returnsCursor()) {
    result = executeForCursor(sqlSession, args);
  } else {
    Object param = method.convertArgsToSqlCommandParam(args);
    result = sqlSession.selectOne(command.getName(), param);
  }
  break;

When you come to the executeformany method, you can see the operation of executing the query. Since there is no paging query here, go else


if (method.hasRowBounds()) {
  RowBounds rowBounds = method.extractRowBounds(args);
  result = sqlSession.<E>selectList(command.getName(), param, rowBounds);
} else {
  result = sqlSession.<E>selectList(command.getName(), param);
}

Go to the defaultsqlsession#selectlist method

@Override
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
  try {
    //Get the mappedstatement object according to the key (namespace + ". + ID)
    //The mappedstatement object encapsulates the parsed SQL information
    MappedStatement ms = configuration.getMappedStatement(statement);
    //Executing queries through cacheingexecution #query
    return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
  } catch (Exception e) {
    throw ExceptionFactory.wrapException("Error querying database.  Cause: " + e, e);
  } finally {
    ErrorContext.instance().reset();
  }
}

CachingExecutor#query

@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
 //Resolve SQL to executable SQL
 BoundSql boundSql = ms.getBoundSql(parameter);
 //Get cached key
 CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
 //Execute query
 return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}

MappedStatement#getBoundSql

public BoundSql getBoundSql(Object parameterObject) {
 //Parse SQL
  BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
  List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
  if (parameterMappings == null || parameterMappings.isEmpty()) {
    boundSql = new BoundSql(configuration, boundSql.getSql(), parameterMap.getParameterMappings(), parameterObject);
  }

  //Check for nested resultmaps
  // check for nested result maps in parameter mappings (issue #30)
  for (ParameterMapping pm : boundSql.getParameterMappings()) {
    String rmId = pm.getResultMapId();
    if (rmId != null) {
      ResultMap rm = configuration.getResultMap(rmId);
      if (rm != null) {
        hasNestedResultMaps |= rm.hasNestedResultMaps();
      }
    }
  }

  return boundSql;
}

From the above, this statement is a dynamicsqlsource because the SQL contains element nodes. This brings you to dynamicsqlsource#getboundsql.
rootSqlNode.apply(context); This code is executing SQL parsing.

@Override
public BoundSql getBoundSql(Object parameterObject) {
  DynamicContext context = new DynamicContext(configuration, parameterObject);
  //Perform SQL parsing
  rootSqlNode.apply(context);
  SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
  Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass();
  SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());
  BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
  for (Map.Entry<String, Object> entry : context.getBindings().entrySet()) {
    boundSql.setAdditionalParameter(entry.getKey(), entry.getValue());
  }
  return boundSql;
}

Hit the breakpoint, follow the parsing process, and come to the code that parses the foreach tag, foreachsqlnode#apply

@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;
  //Resolve open attribute
  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
    //The element in the collection is integer, otherwise
    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 {
      //Using the index attribute
      applyIndex(context, i, uniqueNumber);
      //Using the item property
      applyItem(context, o, uniqueNumber);
    }
    //When the # sign is used in the foreach, the variable is replaced with a placeholder (similar to _frch_width_0) (statictextsqlnode)
    //When the $symbol is used, the value is spliced directly into the SQL (textsqlnode)
    contents.apply(new FilteredDynamicContext(configuration, context, index, item, uniqueNumber));
    if (first) {
      first = !((PrefixedContext) context).isPrefixApplied();
    }
    context = oldContext;
    i++;
  }
  applyClose(context);
  return true;
}

private void applyItem(DynamicContext context, Object o, int i) {
    if (item != null) {
        //Bind the relationship between item attribute value and collection value in parameter mapping
        //First time: (width: 1)
        //Second: (width: 2)
        //Third time: (width: 3)
        context.bind(item, o);
        //In parameter mapping, bind the relationship between the processed item attribute value and the collection value
        //First time: (_frch_width_0:1)
        //Second time: (_frch_width_1:2)
        //Third time: (_frch_width_2:3)
        context.bind(itemizeItem(item, i), o);
    }
  }

Here, the result is clear. When parsing the foreach tag, each cycle will bind the item attribute value to the value in the parameter set, and the mapping relationship of (width: 3) will be retained at the end. After parsing the foreach tag, the last if tag will be parsed. At this time, when judging whether the if tag is valid, the answer is true, Therefore, a wrong SQL is finally spliced.

In version 3.4.5, context. Getbindings(). Remove (item) is added to the code; Remove the parameter mapping in bindings after the foreach tag is resolved. The following is the source code:

@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 || separator == null) {
      context = new PrefixedContext(context, "");
    } else {
      context = new PrefixedContext(context, separator);
    }
    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);
  //After parsing the foreach tag, remove the item from the bindings
  context.getBindings().remove(item);
  context.getBindings().remove(index);
  return true;
}

This is the end of this article about the solution to the problem of value coverage caused by the same variable name in mybatis #foreach. For more information about the coverage of the same variable name in mybatis #foreach, please search the previous articles of developeppaer or continue to browse the relevant articles below. I hope you will support developeppaer in the future!

Recommended Today

SQL exercise 20 – Modeling & Reporting

This blog is used to review and sort out the common topic modeling architecture, analysis oriented architecture and integration topic reports in data warehouse. I have uploaded these reports to GitHub. If you are interested, you can have a lookAddress:https://github.com/nino-laiqiu/TiTanI recorded a relatively complete development process in my hexo blog deployed on GitHub. You can […]