Mybatis #foreach中相同的變量名導致值覆蓋的問題解決
背景
使用Mybatis中執行如下查詢:
單元測試
@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 id="findByCondition" 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:
DEBUG [main] – ==> Preparing: select * from test WHERE width in ( ? , ? , ? ) and width = ?
DEBUG [main] – ==> Parameters: 1(Integer), 2(Integer), 3(Integer), 3(Integer)
Mybatis版本
<dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.4.1</version> </dependency>
這是公司的老項目,在迭代的過程中遇到瞭此問題,以此記錄!
PS: 此bug在mybatis-3.4.5版本中已經解決。並且Mybatis維護者也建議不要在item/index中使用重復的變量名。
問題原因(簡略版)
- 在獲取到DefaultSqlSession之後,會獲取到Mapper接口的代理類,通過調用代理類的方法來執行查詢
- 真正執行數據庫查詢之前,需要將可執行的SQL拼接好,此操作在DynamicSqlSource#getBoundSql方法中執行
- 當解析到foreach標簽時,每次循環都會緩存一個item屬性值與變量值之間的映射(如:width:1),當foreach標簽解析完成後,緩存的參數映射關系中就保留瞭一個(width:3)
- 當解析到最後一個if標簽時,由於width變量有值,因此if判斷為true,正常執行拼接,導致出錯
- 3.4.5版本中,在foreach標簽解析完成後,增加瞭兩行代碼來解決這個問題。
//foreach標簽解析完成後,從bindings中移除item context.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 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 mapperElement(root.evalNode("mappers")); } catch (Exception e) { throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e); } }
來到mapperElement方法
//本次mappers配置:<mapper resource="xml/CommomMapper.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) { //因此走這裡,讀取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|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) { //創建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拼接,判斷是否已經存在此id if (!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 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) //獲取SqlSource對象,langDriver為默認的XMLLanguageDriver,在new Configuration時設置 //若sql中包含元素節點或$,則返回DynamicSqlSource,否則返回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); }
二、獲取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 //如果在配置文件中開啟瞭緩存(默認開啟),則是CachingExecutor final 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方法
@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); } } //緩存 final MapperMethod mapperMethod = cachedMapperMethod(method); //執行操作:select|insert|update|delete return 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.<E>selectList(command.getName(), param, rowBounds); } else { result = sqlSession.<E>selectList(command.getName(), param); }
來到DefaultSqlSession#selectList方法中
@Override public <E> List<E> 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
@Override public <E> List<E> 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) { //解析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); } //檢查是否有嵌套的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解析。
@Override public 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<String, Object> entry : context.getBindings().entrySet()) { boundSql.setAdditionalParameter(entry.getKey(), entry.getValue()); } return boundSql; }
打上斷點,跟著解析流程,來到解析foreach標簽的代碼,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; //解析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,走else 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 { //使用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中的參數映射。以下是源碼:
@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); //foreach標簽解析完成後,從bindings中移除item context.getBindings().remove(item); context.getBindings().remove(index); return true; }
到此這篇關於Mybatis #foreach中相同的變量名導致值覆蓋的問題解決的文章就介紹到這瞭,更多相關Mybatis #foreach相同變量名覆蓋內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!
推薦閱讀:
- 解析Mybatis SqlSessionFactory初始化原理
- 詳解Mybatis的緩存
- Java Mybatis的初始化之Mapper.xml映射文件的詳解
- MyBatis源碼解析——獲取SqlSessionFactory方式
- MyBatis3源碼解析之如何獲取數據源詳解