背景
使用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