Mybatis|Mybatis #foreach中相同的变量名导致值覆盖的问题解决

目录

  • 背景
  • 问题原因(简略版)
  • Mybatis流程源码解析(长文警告,按需自取)
    • 一、获取SqlSessionFactory
    • 二、获取SqlSession
    • 三、执行SQL

背景
使用Mybatis中执行如下查询:
单元测试
@Testpublic 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 list = new ArrayList<>(); list.add(1); list.add(2); list.add(3); queryCondition.setWidthList(list); System.out.println(mapper.findByCondition(queryCondition)); }}

XML
select * from testand id = #{id,jdbcType=INTEGER}#{width,jdbcType=INTEGER}and width = #{width,jdbcType=INTEGER}

打印的SQL:
DEBUG [main] - ==>Preparing: select * from test WHERE width in ( ? , ? , ? ) and width = ?
DEBUG [main] - ==> Parameters: 1(Integer), 2(Integer), 3(Integer), 3(Integer)

Mybatis版本
org.mybatismybatis3.4.1

这是公司的老项目,在迭代的过程中遇到了此问题,以此记录!
PS: 此bug在mybatis-3.4.5版本中已经解决。并且Mybatis维护者也建议不要在item/index中使用重复的变量名。
Mybatis|Mybatis #foreach中相同的变量名导致值覆盖的问题解决
文章图片

Mybatis|Mybatis #foreach中相同的变量名导致值覆盖的问题解决
文章图片


问题原因(简略版)
  • 在获取到DefaultSqlSession之后,会获取到Mapper接口的代理类,通过调用代理类的方法来执行查询
  • 真正执行数据库查询之前,需要将可执行的SQL拼接好,此操作在DynamicSqlSource#getBoundSql方法中执行
  • 当解析到foreach标签时,每次循环都会缓存一个item属性值与变量值之间的映射(如:width:1),当foreach标签解析完成后,缓存的参数映射关系中就保留了一个(width:3)
  • 当解析到最后一个if标签时,由于width变量有值,因此if判断为true,正常执行拼接,导致出错
  • 3.4.5版本中,在foreach标签解析完成后,增加了两行代码来解决这个问题。
//foreach标签解析完成后,从bindings中移除itemcontext.getBindings().remove(item); context.getBindings().remove(index);


Mybatis流程源码解析(长文警告,按需自取)

一、获取SqlSessionFactory
入口,跟着build方法走
//获取SqlSessionFactory, 解析完成后,将XML中的内容封装到一个Configuration对象中,//使用此对象构造一个DefaultSqlSessionFactory对象,并返回SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

来到SqlSessionFactoryBuilder#build方法
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {try {//获取XMLConfigBuilder,在XMLConfigBuilder的构造方法中,会创建XPathParser对象//在创建XPathParser对象时,会将mybatis-config.xml文件转换成Document对象XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties); //调用XMLConfigBuilder#parse方法开始解析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.}}}

跟着parse方法走,来到XMLConfigBuilder#parseConfiguration方法
private void parseConfiguration(XNode root) {try {Properties settings = settingsAsPropertiess(root.evalNode("settings")); //issue #117 read properties firstpropertiesElement(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 #631environmentsElement(root.evalNode("environments")); databaseIdProviderElement(root.evalNode("databaseIdProvider")); typeHandlerElement(root.evalNode("typeHandlers")); //这里解析mappermapperElement(root.evalNode("mappers")); } catch (Exception e) {throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e); }}

来到mapperElement方法
//本次mappers配置: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) {//因此走这里,读取xml文件,并开始解析ErrorContext.instance().resource(resource); InputStream inputStream = Resources.getResourceAsStream(resource); //这里同上文创建XMLConfigBuilder对象一样,在内部构造时,也将xml文件转换为了一个Document对象XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments()); //解析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."); }}}}}

XMLMapperBuilder类,负责解析SQL语句所在XML中的内容
//parse方法public void parse() {if (!configuration.isResourceLoaded(resource)) {//解析mapper标签configurationElement(parser.evalNode("/mapper")); configuration.addLoadedResource(resource); bindMapperForNamespace(); }parsePendingResultMaps(); parsePendingChacheRefs(); parsePendingStatements(); }//configurationElement方法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")); //解析各种类型的SQL语句:select|insert|update|deletebuildStatementFromContext(context.evalNodes("select|insert|update|delete")); } catch (Exception e) {throw new BuilderException("Error parsing Mapper XML. Cause: " + e, e); }}private void buildStatementFromContext(List list, String requiredDatabaseId) {for (XNode context : list) {//创建XMLStatementBuilder对象final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId); try {//解析statementParser.parseStatementNode(); } catch (IncompleteElementException e) {configuration.addIncompleteStatement(statementParser); }}}

XMLStatementBuilder负责解析单个select|insert|update|delete节点
public void parseStatementNode() {String id = context.getStringAttribute("id"); String databaseId = context.getStringAttribute("databaseId"); //判断databaseId是否匹配,将namespace+'.'+id拼接,判断是否已经存在此idif (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {return; }Integer fetchSize = context.getIntAttribute("fetchSize"); Integer timeout = context.getIntAttribute("timeout"); String parameterMap = context.getStringAttribute("parameterMap"); //获取参数类型String parameterType = context.getStringAttribute("parameterType"); //获取参数类型的class对象Class parameterTypeClass = resolveClass(parameterType); String resultMap = context.getStringAttribute("resultMap"); String resultType = context.getStringAttribute("resultType"); String lang = context.getStringAttribute("lang"); LanguageDriver langDriver = getLanguageDriver(lang); //获取resultType的class对象Class resultTypeClass = resolveClass(resultType); String resultSetType = context.getStringAttribute("resultSetType"); StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString())); ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType); //获取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 parsingXMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant); includeParser.applyIncludes(context.getNode()); // Parse selectKey after includes and remove them.processSelectKeyNodes(id, parameterTypeClass, langDriver); // Parse the SQL (pre:and were parsed and removed)//获取SqlSource对象,langDriver为默认的XMLLanguageDriver,在new Configuration时设置//若sql中包含元素节点或$,则返回DynamicSqlSource,否则返回RawSqlSourceSqlSource 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); }


二、获取SqlSession
由上文可知,此处的SqlSessionFactory使用的是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); //创建执行器,默认是SimpleExecutor//如果在配置文件中开启了缓存(默认开启),则是CachingExecutorfinal Executor executor = configuration.newExecutor(tx, execType); //返回DefaultSqlSession对象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(); }}

这里获取到了一个DefaultSqlSession对象

三、执行SQL
获取CommonMapper的对象,这里CommonMapper是一个接口,因此是一个代理对象,代理类是MapperProxy
org.apache.ibatis.binding.MapperProxy@72cde7cc

执行Query方法,来到MapperProxy的invoke方法
@Overridepublic 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); }}//缓存final MapperMethod mapperMethod = cachedMapperMethod(method); //执行操作:select|insert|update|deletereturn mapperMethod.execute(sqlSession, args); }

执行操作时,根据SELECT操作,以及返回值类型(反射方法获取)确定executeForMany方法
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;

来到executeForMany方法中,就可以看到执行查询的操作,由于这里没有进行分页查询,因此走else
if (method.hasRowBounds()) {RowBounds rowBounds = method.extractRowBounds(args); result = sqlSession.selectList(command.getName(), param, rowBounds); } else {result = sqlSession.selectList(command.getName(), param); }

来到DefaultSqlSession#selectList方法中
@Overridepublic List selectList(String statement, Object parameter, RowBounds rowBounds) {try {//根据key(namespace+"."+id)来获取MappedStatement对象//MappedStatement对象中封装了解析好的SQL信息MappedStatement ms = configuration.getMappedStatement(statement); //通过CachingExecutor#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
@Overridepublic List query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException { //解析SQL为可执行的SQL BoundSql boundSql = ms.getBoundSql(parameter); //获取缓存的key CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql); //执行查询 return query(ms, parameter, rowBounds, resultHandler, key, boundSql); }

MappedStatement#getBoundSql
public BoundSql getBoundSql(Object parameterObject) { //解析SQLBoundSql boundSql = sqlSource.getBoundSql(parameterObject); List parameterMappings = boundSql.getParameterMappings(); if (parameterMappings == null || parameterMappings.isEmpty()) {boundSql = new BoundSql(configuration, boundSql.getSql(), parameterMap.getParameterMappings(), parameterObject); }//检查是否有嵌套的ResultMap// 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; }

由上文,此次语句由于SQL中包含元素节点,因此是DynamicSqlSource。由此来到DynamicSqlSource#getBoundSql。
rootSqlNode.apply(context); 这段代码便是在执行SQL解析。
@Overridepublic BoundSql getBoundSql(Object parameterObject) {DynamicContext context = new DynamicContext(configuration, parameterObject); //执行SQL解析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 entry : context.getBindings().entrySet()) {boundSql.setAdditionalParameter(entry.getKey(), entry.getValue()); }return boundSql; }

打上断点,跟着解析流程,来到解析foreach标签的代码,ForEachSqlNode#apply
@Overridepublic boolean apply(DynamicContext context) {Map bindings = context.getBindings(); final Iterable iterable = evaluator.evaluateIterable(collectionExpression, bindings); if (!iterable.iterator().hasNext()) {return true; }boolean first = true; //解析open属性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//集合中的元素是Integer,走elseif (o instanceof Map.Entry) {@SuppressWarnings("unchecked")Map.Entry mapEntry = (Map.Entry) o; applyIndex(context, mapEntry.getKey(), uniqueNumber); applyItem(context, mapEntry.getValue(), uniqueNumber); } else {//使用index属性applyIndex(context, i, uniqueNumber); //使用item属性applyItem(context, o, uniqueNumber); }//当foreach中使用#号时,会将变量替换为占位符(类似__frch_width_0)(StaticTextSqlNode)//当使用$符号时,会将值直接拼接到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) {//在参数映射中绑定item属性值与集合值的关系//第一次:(width:1)//第二次:(width:2)//第三次:(width:3)context.bind(item, o); //在参数映射中绑定处理后的item属性值与集合值的关系//第一次:(__frch_width_0:1)//第二次:(__frch_width_1:2)//第三次:(__frch_width_2:3)context.bind(itemizeItem(item, i), o); }}

到这里,结果就清晰了,在解析foreach标签时,每次循环都会将item属性值与参数集合中的值进行绑定,到最后就会保留(width:3)的映射关系,而在解析完foreach标签后,会解析最后一个if标签,此时在判断if标签是否成立时,答案是true,因此最终拼接出来一个错误的SQL。

在3.4.5版本中,代码中增加了context.getBindings().remove(item); 在foreach标签解析完成后移除bindings中的参数映射。以下是源码:
@Overridepublic boolean apply(DynamicContext context) {Map 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 #709if (o instanceof Map.Entry) {@SuppressWarnings("unchecked")Map.Entry mapEntry = (Map.Entry) 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); //foreach标签解析完成后,从bindings中移除itemcontext.getBindings().remove(item); context.getBindings().remove(index); return true; }

【Mybatis|Mybatis #foreach中相同的变量名导致值覆盖的问题解决】到此这篇关于Mybatis #foreach中相同的变量名导致值覆盖的问题解决的文章就介绍到这了,更多相关Mybatis #foreach相同变量名覆盖内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

    推荐阅读