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

网友投稿 819 2022-12-23

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

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

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

背景

使用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 list = new ArrayList<>();

list.add(1);

list.add(2);

list.add(3);

queryCondition.setWidthList(list);

System.out.println(mapper.findByCondition(queryCondition));

}

}

XML

select * from test

and 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.mybatis

mybatis

3.4.1

这是公司的老项目,在迭代的过程中遇到了此问题,以此记录!

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配置:

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 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: and 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.MapperProxhttp://y@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.selectList(command.getName(), param, rowBounds);

} else {

result = sqlSession.selectList(command.getName(), param);

}

来到DefaultSqlSession#selectList方法中

@Override

public 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

@Override

public 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) {

//解析SQL

BoundSql 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解析。

@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 entry : context.getBindings().entrySet()) {

boundSql.setAdditionalParameter(entry.getKey(), entry.getValue());

}

return boundSql;

}

打上断点,跟着解析流程,来到解析foreach标签的代码,ForEachSqlNode#apply

@Override

public 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 uniquhttp://eNumber = context.getUniqueNumber();

// Issue #709

//集合中的元素是Integer,走else

if (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中的参数映射。以下是源码:

@Override

public 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 #709

if (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中移除item

context.getBindings().remove(item);

context.getBindings().remove(index);

return true;

}

版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。

上一篇:车载物联网设备(物联网 车联网)
下一篇:重庆嵌入式智能车载终端(重庆重庆车联网科技产业园)
相关文章

 发表评论

暂时没有评论,来抢沙发吧~