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>
Mybatis:解决调用带有集合类型形参的mapper方法时,集合参数为空或null的问题的更多相关文章
- day007 列表类型、元祖类型、 字典类型、 集合类型的内置方法
目录 列表数据类型的内置方法 作用 定义方式 优先掌握的方法 需要掌握的方法 元祖类型的内置方法 作用 定义方式 优先掌握的方法(参考列表方法) 字典类型的内置方法 作用 定义方式 优先掌握的方法 需 ...
- 解决sql语句中参数为空(null)不会更新参数的问题
用的mybatis自动生成的 情景: 修改页面中,修改某个字段,修改前有数据,修改后为空. mybatis中一般用到 如:(这种直接忽略为空的字段,不能更新空字段参数) <update id=& ...
- vue调用子组件方法时,参数传不过去
有可能是因为子组件方法用了 async await 子组件去掉async就好了
- 表达式树练习实践:C#值类型、引用类型、泛型、集合、调用函数
目录 表达式树练习实践:C#值类型.引用类型.泛型.集合.调用函数 一,定义变量 二,访问变量/类型的属性字段和方法 1. 访问属性 2. 调用函数 三,实例化引用类型 四,实例化泛型类型于调用 五, ...
- Swift中的集合类型
一.引子: 在2014年10月TIOBE编程语言排行榜中,Swift位居第18位,从2014WWDC发布会首次公布至今不到半年时间,swift一直受到编程人 员的追捧,其热衷程度并不亚于当红巨星Tay ...
- C# - 集合类 - 集合类型
Stack类 ns:System.Collections 此类模仿了一个简单的先进后出(LIFO)的集合 它实现了ICollection接口 可以通过三种方式创建Stack类对象 Stack stac ...
- 2015/9/4 Python基础(8):映射和集合类型
Python里唯一的映射类型是字典.映射类型对象里,hash值(key)和指向的对象(值)是一对多的关系.字典对象是可变的,这一点上很像列表,它也可以存储任意个数任意类型的Python对象,其中包括容 ...
- Python中的集合类型分类和集合类型操作符解析
集合类型 数学上,把set称作由不同的元素组成的集合,集合(set)的成员通常被称作集合元素(set elements). Python把这个概念引入到它的集合类型对象里.集合对象是一组无 ...
- Swift学习笔记(5):集合类型
目录: 数组:Array 集合:Set 字典:Dictionary Swift提供Array(有序集合数据).Set(无序无重复集合)和Dictionary(无序键值对集合)三种基本集合类型来存储明确 ...
随机推荐
- FastJson远程命令执行漏洞学习笔记
FastJson远程命令执行漏洞学习笔记 Fastjson简介 fastjson用于将Java Bean序列化为JSON字符串,也可以从JSON字符串反序列化到JavaBean.fastjson.ja ...
- KingbaseES 两表关联Update的两种写法与性能
熟悉oracle 的人都知道,对于两表的关联更新,其执行计划主要有 Filter 和 Outer Join 两种方式.对于大批量数据的update,Join方式明显是更优的选择.KingbaseES ...
- 安装docker及使用docker安装其他软件(手动挂载数据卷)
中秋明月,豪门有,贫家也有,极慰人心 Linux安装docker 可以参考官方的安装文档 centos安装docker: https://docs.docker.com/engine/install/ ...
- salesforce零基础学习(一百一十七)salesforce部署方式及适用场景
本篇参考:https://architect.salesforce.com/decision-guides/migrate-change https://developer.salesforce.co ...
- Openstack Neutron:二层技术和实现
目录 - 二层的实现 - 1.本地联通与隔离: - Linux bridge实现方式: - local - Flat - VLAN - VXLAN - Open vswitch实现方式 - local ...
- 发布日志- kratos v2.1.4 发布!
v2.1.4 release https://github.com/go-kratos/kratos/releases/tag/v2.1.4 New Features feat(registry/co ...
- acme.sh官方中文说明文档
转载自:https://github.com/acmesh-official/acme.sh/wiki/说明 acme.sh 实现了 acme 协议, 可以从 letsencrypt 生成免费的证书. ...
- Docker 容器默认root账号运行,很不安全!
文章转载自:https://mp.weixin.qq.com/s/AeZoEKZBWFYwyhgicpgD4Q
- Elasticsearch Reindex性能提升10倍+实战
文章转载自: https://mp.weixin.qq.com/s?__biz=MzI2NDY1MTA3OQ==&mid=2247484134&idx=1&sn=750249a ...
- (五)JPA - 原生SQL实现增删改查
6.原生SQL JPA除了对JPQL提供支持外,还对原生SQL语句也提供了支持.下面小节一起来看看吧. 6.1 查询单个 示例代码: @Test public void getSingle() { E ...