我把之前发布在简书的两篇文章通过拦截器Interceptor优化Mybatis的in查询 和Mybatis中foreach标签内list为空的解决方案进行了整合,整理为本文的内容。此外,我还对代码部分进行了优化,增加了必要的注释。希望大家阅读愉快。
在工作中,我们经常会因为在mybatis中的不严谨写法,导致foreach解析后的sql语句产生in()或values()的情况,而这种情况不符合SQL的语法,最终会导致bad SQL grammar []; nested exception is com.mysql.jdbc.exceptions.jdbc4.MySQLSyntaxErrorException的问题。
看到这个报错,大家肯定就会意识到是sql语句的问题,那么我们该如何解决这个问题呢?
网络上有一些现成的解决方案:
1、对list判null和判空来处理
<if test="list != null and list.size>0">
do something
if>
复制代码
这种方案解决了sql语句有误的问题,但同时产生了一个新的逻辑问题。本来预想的in一个空列表,查询结果应该是没有数据才对,但实际上这么写会导致这个in条件失效,这就导致了执行结果并非不是我们想要的问题。
2、对list做双重判断。第一重判断和上面的解决方案一致,增加的第二重判断是为了保证如果list为空列表则只能查到空列表
<if test="list != null and list.size>0">
do something
if>
<if test="list!=null and list.size==0">
and 1=0
if>
复制代码
这种方案能解决sql报错的问题,也不会产生逻辑错误的情况。但是这个写法有点繁琐,每次遇到这种情况都需要特殊判断,会极大降低开发的效率。
那么还有更优雅的写法么?
答案肯定是有的,我们可以通过拦截器Interceptor来优雅的解决这个问题。其他业务同学,还是和往常一样,只需要在xml中判断list非null就可以了。
@Intercepts({
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class,RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}),
@Signature(type = Executor.class, method = "update", args = {MappedStatement.class,Object.class})})
public class EmptyCollectionIntercept implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
//通过invocation.getArgs()可以得到当前执行方法的参数
//第一个args[0]是MappedStatement对象,第二个args[1]是参数对象parameterObject。
final Object[] args = invocation.getArgs();
MappedStatement mappedStatement = (MappedStatement) args[0];
Object parameter = args[1];
if (parameter == null) {
Class parameterType = mappedStatement.getParameterMap().getType();
// 实际执行时的参数值为空,但mapper语句上存在输入参数的异常状况,返回默认值
if (parameterType != null) {
return getDefaultReturnValue(invocation);
}
return invocation.proceed();
}
BoundSql boundSql = mappedStatement.getBoundSql(parameter);
if (isHaveEmptyList(boundSql.getSql())) {
return getDefaultReturnValue(invocation);
}
return invocation.proceed();
}
@Override
public Object plugin(Object target) {
//只拦截Executor对象,减少目标被代理的次数
if (target instanceof Executor) {
return Plugin.wrap(target, this);
} else {
return target;
}
}
@Override
public void setProperties(Properties properties) {
}
/**
* 返回默认的值,list类型的返回空list,数值类型的返回0
*
* @param invocation
* @return
*/
private Object getDefaultReturnValue(Invocation invocation) {
Class returnType = invocation.getMethod().getReturnType();
if (returnType.equals(List.class)) {
return Lists.newArrayList();
} else if (returnType.equals(Integer.TYPE) || returnType.equals(Long.TYPE)
|| returnType.equals(Integer.class) || returnType.equals(Long.class)) {
return 0;
}
return null;
}
/**
* 去除字符中的干扰项,避免字符串中的内容干扰判断。
*
* @param sql
* @return
*/
private static String removeInterference(String sql) {
Pattern pattern = Pattern.compile("[\"|'](.*?)[\"|']");
Matcher matcher = pattern.matcher(sql);
while (matcher.find()) {
String replaceWorld = matcher.group();
sql = sql.replace(replaceWorld, "''");
}
return sql;
}
/**
* 判断是否存在空list
*
* @param sql
* @param methodName
* @return
*/
private static Boolean isHaveEmptyList(String sql, String methodName) {
sql = removeInterference(sql);
List keyWorldList = Lists.newArrayList("in", "values");
Boolean isHaveEmptyList = Boolean.FALSE;
for (String keyWorld : keyWorldList) {
List indexList = Lists.newArrayList();
//获取关键词后的index,关键词前必须为空白字符,但以关键词开头的单词也会被匹配到,例如index
Pattern pattern = Pattern.compile("\\s(?i)" + keyWorld);
Matcher matcher = pattern.matcher(sql);
while (matcher.find()) {
indexList.add(matcher.end());
}
if (CollectionUtils.isNotEmpty(indexList)) {
isHaveEmptyList = checkHaveEmptyList(sql, indexList);
if (isHaveEmptyList) {
break;
}
}
}
return isHaveEmptyList;
}
/**
* 判断sql在indexList的每个index后是否存在存在空列表的情况
*
* @param sql
* @param indexList keyWorld在sql中的位置
* @return
*/
private static Boolean checkHaveEmptyList(String sql, List indexList) {
Boolean isHaveEmptyList = Boolean.FALSE;
//获取()内的内容
Pattern p2 = Pattern.compile("(?<=\\()(.+?)(?=\\))");
for (Integer index : indexList) {
String subSql = sql.substring(index);
//如果关键词之后无任何sql语句,则sql语句结尾为关键词,此时判定为空列表
if (StringUtils.isEmpty(subSql)) {
isHaveEmptyList = Boolean.TRUE;
break;
}
//关键词后必须是(或者是空字符或者是换行符等才有继续判断的意义,避免sql中存在以关键词in或values开头的单词的情况干扰判断
boolean flag = subSql.startsWith("(")
|| subSql.startsWith(" ")
|| subSql.startsWith("\n")
|| subSql.startsWith("\r");
if (!flag) {
continue;
}
subSql = subSql.trim();
//如果关键词后的sql语句trim后不以(开头,也判定为空列表
if (!subSql.startsWith("(")) {
isHaveEmptyList = Boolean.TRUE;
break;
}
Matcher m2 = p2.matcher(subSql);
//如果括号()内的内容trim后为空,则判定为空列表
if (m2.find()) {
if (StringUtils.isEmpty(m2.group().trim())) {
isHaveEmptyList = Boolean.TRUE;
break;
}
}
}
return isHaveEmptyList;
}
}
复制代码
具体的判断过程如上所示,关键代码已写注释,阅读起来应该不费事。
最后,把我们写的拦截器加入到sqlSessionFactory的plugins即可投入使用。
"plugins">
"com.yibao.broker.intercepts.listIntercept.MyBatisCheckEmptyBeforeExecuteInterceptor">
"properties">
property-key=property-value
复制代码
关于mybatis的拦截器Interceptor,有兴趣的可以自行查阅一下。
这个插件在我司已使用了1年多,目前正常运作。如果大家在使用过程中有什么问题,欢迎留言联系。