mybatis forEach标签item影响其他标签判断的问题

mapper.xml文件中,多个标签中存在属性中使用同名变量,若前边的标签修改了变量的值,则前边的标签可能会影响后边的标签(一般是forEache标签影响后边标签),示例:

 1 <?xml version="1.0" encoding="UTF-8" ?>
 2 <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
 3 <mapper namespace="com.mrlu.mybatis.dao.AccountDao" >
 4     <resultMap id="BaseResultMap" type="com.mrlu.mybatis.domain.Account">
 5         <result column="id" property="id" />
 6         <result column="user_id" property="userId" />
 7         <result column="num" property="num" />
 8     </resultMap>
 9 
10     <select id="selectById" parameterType="java.util.List" resultMap="BaseResultMap">
11         SELECT *
12         from account
13         WHERE
14         <foreach collection="list" item="id" open="id in (" close=")" separator=",">
15             #{id}
16         </foreach>
17         <if test="id != null"> <!--注意此处,if标签中变量id和forEach标签中item属性变量名称相同-->
18             and id = #{id}
19         </if>
20     </select>
21 </mapper>

上述mapper.xml文件的配置,调用selectById()方法时,传入的参数是List,若List不为空,则if标签每次都会执行,并且if标签中id的值是参数List中遍历的最后一个值

测试方法:

 1 public static void main(String[] args){
 2         String resource = "mybatis-config.xml";
 3         try {
 4             Reader reader = Resources.getResourceAsReader(resource);
 5             SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(reader);
 6             SqlSession sqlSession  = sqlSessionFactory.openSession();
 7             AccountDao accountDao = sqlSession.getMapper(AccountDao.class);
 8 
 9             Account account = accountDao.selectById(Arrays.asList(new Integer[]{1,2}));
10             System.out.println(account);
11         } catch (IOException e) {
12             e.printStackTrace();
13         }
14     }

MySQL执行日志:

可以看到,我们传递的参数是一个List,没有传名称为id的参数,但是if标签能正常通过。所以在mapper.xml配置文件中,同一个SQL语句中的不同标签(主要针对会修改属性中变量值的标签,如forEach),尽量使用不同的变量名称

原理:

1. 对mapper.xml解析过程: XMLMapperBuilder(解析resultMap, cache等) --> XMLStatementBuilder(解析SQL语句的id,parameterType等属性) --> XMLScriptBuilder(解析SQL语句和内部标签(如if, forEach等))

  1.1 每个SQL语句标签会生成一个SqlSource对象(一般是DynamicSqlSource),里边包含一个rootSqlNode根节点(一般是MixedSqlNode),根节点里边又包含多个子节点(包括StaticTextSqlNode, IfSqlNode, ForEachSqlNode, MixedSqlNode等等)

  1.2 如果SQL语句标签中只有普通的SQL语句,没有其他标签,则生成得到SqlSource对象是RawSqlSource对象,RawSqlSource里又是一个StaticSqlSource对象,StaticSqlSource对象包含Sql语句(占位符?已经将#{}替换掉)和参数类型

2. DynamicSqlSource和RawSqlSource的区别:

  2.1 DynamicSqlSource在解析的过程中得到的sql语句依然包含#{},并且还有一些节点标签,在最终执行的时候,才会根据传入的参数来处理节点标签,得到预编译的SQL语句(?替换掉#{})

  2.2 RawSqlSource在解析的时候得到的Sql语句已经是最终预编译的SQL语句

3. 对开头的这个问题的原理解析:

  3.1 最终执行的SQL语句是通过SqlSource的getBoundSql()方法来得到的,此处只看DynamicSqlSource源码,因为RawSqlSource没有子节点标签,不存在上述问题:

 1 public BoundSql getBoundSql(Object parameterObject) {
 2     DynamicContext context = new DynamicContext(configuration, parameterObject);
 3     rootSqlNode.apply(context);
 4     SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
 5     Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass();
 6     SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());
 7     BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
 8     for (Map.Entry<String, Object> entry : context.getBindings().entrySet()) {
 9       boundSql.setAdditionalParameter(entry.getKey(), entry.getValue());
10     }
11     return boundSql;
12   }

  3.2 首先得到一个DynamicContext

 1 private final ContextMap bindings; //static class ContextMap extends HashMap<String, Object> 
 2 
 3 public DynamicContext(Configuration configuration, Object parameterObject) {
 4     if (parameterObject != null && !(parameterObject instanceof Map)) {
 5       MetaObject metaObject = configuration.newMetaObject(parameterObject);
 6       bindings = new ContextMap(metaObject);
 7     } else {
 8       bindings = new ContextMap(null);
 9     }
10     bindings.put(PARAMETER_OBJECT_KEY, parameterObject); //"_parameter"
11     bindings.put(DATABASE_ID_KEY, configuration.getDatabaseId()); //"_databaseId"
12   }

  Map类型的bindings保存着参数信息,key为参数的名称(若是List则是list,若是Array则是array,若是Bean则是属性名称,若是Map则使用的是map中的key,若是String,Integer或Long等基础类型则需使用"_parameter"获取参数值)

  3.3 调用SqlNode的apply(Context context)方法,一般rootSqlNode是MixedSqlNode,因为可能有多个标签

1 public boolean apply(DynamicContext context) {
2     for (SqlNode sqlNode : contents) { //调用每一个sqlNode的apply()方法
3       sqlNode.apply(context);
4     }
5     return true;
6   }

  3.4 ForEachSqlNode的apply(Context context)方法:

调用之前的context对象的bindings:

 1 public boolean apply(DynamicContext context) {
 2     Map<String, Object> bindings = context.getBindings(); //拿到参数信息
 3     final Iterable<?> iterable = evaluator.evaluateIterable(collectionExpression, bindings);
 4     if (!iterable.iterator().hasNext()) {
 5       return true;
 6     }
 7     boolean first = true;
 8     applyOpen(context);
 9     int i = 0;
10     for (Object o : iterable) { //遍历参数
11       DynamicContext oldContext = context;
12       if (first) {
13         context = new PrefixedContext(context, "");
14       } else {
15         if (separator != null) {
16           context = new PrefixedContext(context, separator);
17         } else {
18           context = new PrefixedContext(context, "");
19         }
20       }
21       int uniqueNumber = context.getUniqueNumber();
22       if (o instanceof Map.Entry) { // Issue #709 
23         @SuppressWarnings("unchecked") 
24         Map.Entry<Object, Object> mapEntry = (Map.Entry<Object, Object>) o;
25         applyIndex(context, mapEntry.getKey(), uniqueNumber);
26         applyItem(context, mapEntry.getValue(), uniqueNumber); //处理item
27       } else {
28         applyIndex(context, i, uniqueNumber);
29         applyItem(context, o, uniqueNumber); //处理item
30       }
31       contents.apply(new FilteredDynamicContext(configuration, context, index, item, uniqueNumber));
32       if (first) first = !((PrefixedContext) context).isPrefixApplied();
33       context = oldContext;
34       i++;
35     }
36     applyClose(context);
37     return true;
38   }
1 private void applyItem(DynamicContext context, Object o, int i) {
2     if (item != null) {
3       context.bind(item, o);
4       context.bind(itemizeItem(item, i), o);
5     }
6   }

 

1 private static String itemizeItem(String item, int i) {
2     return new StringBuilder(ITEM_PREFIX).append(item).append("_").append(i).toString(); //__frch_id_0,代表的是for中每个位置的值
3   }

DynamicContext.bind

1 public void bind(String name, Object value) {
2     bindings.put(name, value);
3   }

 

注意,当处理到ForEachSqlNode对象的时候,会将每个item都put到ContextMap(Map)类型的bindings对象中,所以:

对于最开始XML配置:

传入的参数是 List: 1,2     item指定的是id, 遍历list

1. 处理第一个1, 执行bindings.put("id", 1);  bindings.put("__frch_id_0", 1) ;

2. 处理第二个2,执行bindings.put("id", 2);  bindings.put("__frch_id_1", 1) ;

当处理完ForEachSqlNode对象时,DynamicContext中的bindings(ContextMap)中包含:  此时多出三个Entry(id, __frch_id_0, __frch_id_1)

IfSqlNode的apply(Context context)方法:

1 public boolean apply(DynamicContext context) {
2     if (evaluator.evaluateBoolean(test, context.getBindings())) {//根据DynamicContext的bindings和if表达式判断是否需要if标签中的sql语句
3       contents.apply(context);
4       return true;
5     }
6     return false;
7   }

 

由于在IfSqlNode之前,ForEachSqlNode已经对Context的bindings属性做了修改(添加了三个属性,见上图),此时再拿到Context对象的bindings属性时,已经是修改过的属性,此时就会出现最开始的问题,没有传入id属性,但却能拿到if标签下的sql语句。

总结: 对于会修改标签属性中变量值的标签(如forEach标签的item属性),一般不要和其他标签中变量名称相同,避免相互影响,如果真要相同,把forEach等标签放到不会修改变量值标签的后边

修改后的mapper.xml文件:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.mrlu.mybatis.dao.AccountDao" >
    <resultMap id="BaseResultMap" type="com.mrlu.mybatis.domain.Account">
        <result column="id" property="id" />
        <result column="user_id" property="userId" />
        <result column="num" property="num" />
    </resultMap>

     <select id="selectById" parameterType="java.util.Map" resultMap="BaseResultMap">
             SELECT *
             from account
             WHERE
             <foreach collection="list" item="model" open="id in (" close=")" separator=",">
                 #{model}
             </foreach>
             <if test="id != null"> <!--if中的变量名称是id forEache中是model-->
                 and id = #{id}
             </if>
             limit 0, 1
     </select>
</mapper>

 

测试代码:

 1 public static void main(String[] args){
 2         String resource = "mybatis-config.xml";
 3         try {
 4             Reader reader = Resources.getResourceAsReader(resource);
 5             SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(reader);
 6             SqlSession sqlSession  = sqlSessionFactory.openSession();
 7             AccountDao accountDao = sqlSession.getMapper(AccountDao.class);
 8 
 9             Map map = new HashMap();
10             map.put("list", Arrays.asList(new Integer[]{1,2}));
11             Account account = accountDao.selectById(map);
12             System.out.println(account);
13         } catch (IOException e) {
14             e.printStackTrace();
15         }
16     }

 

MySql日志:

 

转载于:https://www.cnblogs.com/stefanking/articles/5116511.html

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值