Mybatis:解決調用帶有集合類型形參的mapper方法時,集合參數為空或null的問題


此文章有問題,待修改!

使用Mybatis時,有時需要批量增刪改查,這時就要向mapper方法中傳入集合類型(List或Set)參數,下面是一個示例。

// 該文件不完整,只展現關鍵部分
@Mapper
public class UserMapper {
    List<User> selectByBatchIds(List<Long> ids);
}
<!-- 省略不重要代碼,只保留與selectByBatchIds()方法對應的部分 -->
  <select id="selectByBatchIds" parameterType="long" resultMap="user">
    select * from `user` 
      where id in <foreach item="id" collection="ids" open="(" separator="," close=")">#{id}</foreach>;
  </select>

但是如果傳入的集合類型參數為null或空集合會怎樣呢?如果集合類型參數為null,程序調用方法時拋出NullPointerException;如果集合類型參數為空集合,渲染出來的sql語句將會是"select * from `user` where id in ;",執行sql時也會報錯。

這類問題經典的解決辦法有兩種。第一種方法,在調用mapper方法前,檢查方法實參是否為null或空集合;第二種方法:在XXMapper.xml的CRUD元素中使用<if>標簽或<choose>標簽進行判斷,下面是一個改進的XXMapper.xml的示例。

<!-- 省略不重要代碼,只保留與selectByBatchIds()方法相關的片段 -->
  <select id="selectByBatchIds" parameterType="long" resultMap="user">
    <choose>
      <when test="ids != null and ids.size() != 0">
        select * from `user` 
          where id in <foreach item="id" collection="ids" open="(" separator="," close=")">#{id}</foreach>
      </when>
      <otherwise>select * from `user` where false</otherwise>
    </choose>;
  </select>

上面的兩種方法都需要在許多地方增加檢查代碼,顯得不夠優雅,有沒有比較優雅的方法呢?有,使用Mybatis攔截器。攔截器可以攔截mapper方法的執行,根據條件決定mapper方法如何執行,如果傳入的參數為空集合,則返回默認值(空集合、0或null)。下面是一個示例。

  1 package demo.persistence.mybatis.interceptor;
  2 
  3 import org.apache.ibatis.cache.CacheKey;
  4 import org.apache.ibatis.executor.Executor;
  5 import org.apache.ibatis.mapping.BoundSql;
  6 import org.apache.ibatis.mapping.MappedStatement;
  7 import org.apache.ibatis.plugin.Interceptor;
  8 import org.apache.ibatis.plugin.Intercepts;
  9 import org.apache.ibatis.plugin.Invocation;
 10 import org.apache.ibatis.plugin.Signature;
 11 import org.apache.ibatis.session.ResultHandler;
 12 import org.apache.ibatis.session.RowBounds;
 13 import org.jetbrains.annotations.NotNull;
 14 import org.jetbrains.annotations.Nullable;
 15 
 16 import java.lang.reflect.Method;
 17 import java.lang.reflect.Parameter;
 18 import java.util.*;
 19 import java.util.concurrent.ConcurrentHashMap;
 20 import java.util.concurrent.ConcurrentSkipListSet;
 21 
 22 import static org.springframework.util.StringUtils.quote;
 23 import static demo.consts.IntegerType.isIntegerType;
 24 import static demo.consts.RegularExpression.CLASS_METHOD_DELIMITER;
 25 
 26 /**
 27  * 此Mybatis攔截器處理mapper方法中集合類型參數為null或為空的情況。如果集合參數為null或為空,則mapper方法的返回值
 28  * 為空集合、0或null,具體返回值視方法本身的返回值而定。<br />
 29  * 注意:① 有的mapper方法將其所需參數放入Map中,此攔截器不處理此類情況;
 30  * ② 有時,向mapper方法傳遞null參數被視為錯誤,但此攔截器將其當做正常情況處理
 31  */
 32 // Interceptors注解中寫要攔截的的方法簽名,但是此處要攔截的方法不是mapper類中的方法,而是Executor類中的方法。
 33 // 可能Mybatis在執行mapper方法時是通過Executor類中的方法來執行的吧。
 34 @Intercepts({
 35         @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class,
 36                 RowBounds.class, ResultHandler.class}),
 37         @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class,
 38                 RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}),
 39         @Signature(type = Executor.class, method = "update", args = {MappedStatement.class,Object.class})})
 40 public class EmptyCollectionArgsInterceptor implements Interceptor {
 41 
 42     // 緩存具有集合參數的mapper方法名字以及集合參數的名字,執行這些方法時需要檢查它的方法參數是否為null或為空
 43     private final static Map<String, Set<String>> REQUIRE_CHECK = new ConcurrentHashMap<>();
 44     // 緩存沒有集合參數的mapper方法名字,執行這些方法時不需要檢查它的方法參數
 45     private final static Set<String> NOT_CHECK = new ConcurrentSkipListSet<>();
 46 
 47     @Override
 48     public Object intercept(@NotNull Invocation invocation) throws Throwable {
 49         // 獲得Executor方法的實參數組,第一個參數是MappedStatement對象,第二個參數是mapper方法的參數
 50         final Object[] executorMethodArgs = invocation.getArgs();
 51         MappedStatement mappedStatement = (MappedStatement) executorMethodArgs[0];
 52         // 關於mapperMethodArgs變量的說明:
 53         // (1) 如果mapper方法只有一個參數
 54         // ① 如果該參數實際為null,則mapperMethodArgs值為null;
 55         // ② 如果該參數為Map類型且不為null,則mapperMethodArgs的值就是該Map參數的值
 56         // ③ 如果該參數為List類型且不為null,則mapperMethodArgs的類型為MapperMethod.ParamMap(繼承於HashMap),
 57         //    Map中有三對鍵值,它們的值都是該List類型實參,鍵則分別為"collection"、"list"和List形參的名字
 58         // ④ 如果該參數為Set類型且不為null,則mapperMethodArgs的類型為MapperMethod.ParamMap(繼承於HashMap),
 59         //    Map中有兩對鍵值對,它們的值都是該List類型實參,鍵則分別為"collection"和Set形參的名字
 60         // (2) 如果mapper方法有多個參數,無論實參是否為null,mapperMethodArgs的類型始終為MapperMethod.ParamMap,
 61         //     Map中的鍵值對就是mapper方法的形參名字與實參值的對,此時集合類型參數沒有別名
 62         Object mapperMethodArgs = executorMethodArgs[1];
 63         // mapper方法id,就是在XXMapper.xml的CRUD元素中寫的id,而且在該id前加上了對應mapper接口的全限定類名
 64         final String mapperMethodId = mappedStatement.getId();
 65 
 66         // 通過mapperMethodId判斷該mapper方法是否有集合參數。如果mapperMethodId尚未緩存,requireCheck()方法會將其緩存。
 67         if (requireCheck(mapperMethodId)) {
 68             // 如果該mapper方法有集合參數
 69             // 而mapperMethodArgs為null,顯然傳入該mapper方法的實參為null,這時應該返回默認值
 70             if (mapperMethodArgs == null) {
 71                 return getDefaultReturnValue(invocation);
 72             }
 73             // 如果mapperMethodArgs不為null,那么它一定是Map類型的參數
 74             Map<String, ?> argMap = (Map<String, ?>) mapperMethodArgs;
 75             final Set<String> requiredNotEmptyArgs = REQUIRE_CHECK.get(mapperMethodId);
 76             for (String requiredNotEmptyArg : requiredNotEmptyArgs) {
 77                 // 從argMap取出所有集合類型的實參,檢查它是否為null或是否為空。如果是,則返回默認值
 78                 final Object arg = argMap.get(requiredNotEmptyArg);
 79                 if (arg == null || ((Collection<?>) arg).isEmpty()) {
 80                     return getDefaultReturnValue(invocation);
 81                 }
 82             }
 83         }
 84 
 85         // 如果上述檢查沒有問題,則讓mapper方法正常執行
 86         return invocation.proceed();
 87     }
 88 
 89     /**
 90      * 當mapper方法出錯時返回的默認值。
 91      * @return 如果Executor方法返回List類型對象,則此方法返回空List;如果Executor方法返回數字,則此方法返回0;其余情況返回null。
 92      */
 93     private @Nullable Object getDefaultReturnValue(@NotNull Invocation invocation) {
 94         Class<?> returnType = invocation.getMethod().getReturnType();
 95         if (returnType.equals(List.class)) {
 96             return Collections.emptyList();
 97             // isIntegerType()方法判斷Class對象是不是整數Class,自己寫
 98         } else if (isIntegerType(returnType)) {
 99             return 0;
100         }
101         return null;
102     }
103 
104     /**
105      * 檢查mapper方法是否有集合類型參數。<br />
106      * 注意:此方法有副作用。
107      * @param mapperMethodId mapper方法。由mapper類的全限定名和方法名字組成。可由MappedStatement.getId()方法獲取。
108      * @throws ClassNotFoundException 如果未能找到指定的mapper方法的類
109      * @throws NoSuchMethodException 如果未能找到指定的mapper方法
110      */
111     private static boolean requireCheck(String mapperMethodId) throws ClassNotFoundException, NoSuchMethodException {
112         // 如果該方法名字存在於無需檢查方法集合中,說明該方法無需檢查,返回false
113         if (NOT_CHECK.contains(mapperMethodId)) {
114             return false;
115         }
116         // 如果該方法名字存在於需要檢查方法Map中,說明該方法需要檢查,返回true
117         if (REQUIRE_CHECK.containsKey(mapperMethodId)) {
118             return true;
119         }
120 
121         // 如果方法名字不在緩存中,則進行以下操作:
122         // 從完整方法名中分割出全限定類名和方法名
123         // CLASS_METHOD_DELIMITER是類和方法分隔符,自己寫吧
124         final String[] fullClassAndMethod = mapperMethodId.split(CLASS_METHOD_DELIMITER, 2);
125         final String fullQualifiedName = fullClassAndMethod[0];
126         final String methodName = fullClassAndMethod[1];
127         Method targetMethod = null;
128         int paramCount = -1;
129         // 遍歷指定對應類的全部方法,以找到目標方法
130         for (Method method : Class.forName(fullQualifiedName).getMethods()) {
131             // 個人習慣是在mapper接口中定義幾個重載的默認方法,這些默認方法的參數數量比同名的非默認方法的參數數量少,
132             // 所以參數數量最多的方法就是要攔截並檢查的方法
133             if (method.getName().equals(methodName) && method.getParameterCount() > paramCount) {
134                 targetMethod = method;
135                 paramCount = method.getParameterCount();
136             }
137         }
138 
139         if (targetMethod == null) {
140             throw new NoSuchMethodException("Can't find method " + quote(mapperMethodId));
141         }
142         // 檢查目標方法是否有集合參數。如果有,則將該集合參數的名字放入collectionArgNames中。
143         Set<String> collectionArgNames = new HashSet<>();
144         for (Parameter parameter : targetMethod.getParameters()) {
145             if (Collection.class.isAssignableFrom(parameter.getType())) {
146                 collectionArgNames.add(parameter.getName());
147             }
148         }
149         if (collectionArgNames.isEmpty()) {
150             // 如果collectionArgNames為空,說明該方法沒有集合參數,不需要檢查,返回false
151             // 同時將該方法名字存入無需檢查方法集合中
152             NOT_CHECK.add(mapperMethodId);
153             return false;
154         } else {
155             // 如果該collectionArgNames不為空,說明該方法有集合參數,需要檢查,返回true
156             // 同時將該方法名字存入需要檢查方法Map中
157             REQUIRE_CHECK.put(mapperMethodId, collectionArgNames);
158             return true;
159         }
160     }
161 
162 }

要使該攔截器生效,需要在mybatis-config.xml中配置該攔截器,在mybatis-config.xml中添加如下內容即可:

    <plugins>
        <plugin interceptor="demo.persistence.mybatis.interceptor.EmptyCollectionArgsInterceptor" />
    </plugins>

 


免責聲明!

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



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