标签:
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日志:
mybatis forEach标签item影响其他标签判断的问题
标签:
原文地址:http://www.cnblogs.com/stefanking/p/5116511.html