Mybatis#foreach中相同的变量名导致值覆盖

愿天堂没有BUG

共 20004字,需浏览 41分钟

 ·

2021-07-31 08:42


背景

使用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;
}
复制代码
  • 以上便是此问题的小结。




作者:liupjie
链接:https://juejin.cn/post/6989425194741792804
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。



浏览 8
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报