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

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM