-
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>
来源:https://www.cnblogs.com/cnblog-user/p/15364283.html
最新更新
带有参数的装饰器
类装饰器
django中的auth模块与admin后台管理
python的日期处理
字符串常用方法
基本数据类型概述
python-map()函数基本用法
python带你实现任意下载AcFun视频数据~
bbs项目之注册功能
变量的定义和使用
三大常用数据库事务详解之三:事务运行
三大常用关系型数据库事务详解之二:基
三大关系型数据库事务详解之一:基本概
MongoDB常用命令(2)
MongoDB基本介绍与安装(1)
SQLServer触发器调用JavaWeb接口
SQL Server索引的原理深入解析
SqlServer2016模糊匹配的三种方式及效率问题
SQL中Truncate的用法
sqlserver 多表关联时在where语句中慎用tri
VB.NET中如何快速访问注册表
ASP.NET中图象处理过程详解
Vue(1)Vue安装与使用
JavaScript 语言入门
js将一段字符串的首字母转成大写
纯原生html编写的h5视频播放器
H5仿原生app短信验证码vue2.0组件附源码地
TypeScript(4)接口
TypeScript(3)基础类型
TypeScript(2)WebStorm自动编译TypeScript配置