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日志:

