Mybatis系列:解决foreach标签内list为空的问题

时间:2024-10-23 07:31:30

我把之前发布在简书的两篇文章通过拦截器Interceptor优化Mybatis的in查询Mybatis中foreach标签内list为空的解决方案进行了整合,整理为本文的内容。此外,我还对代码部分进行了优化,增加了必要的注释。希望大家阅读愉快。

在工作中,我们经常会因为在mybatis中的不严谨写法,导致foreach解析后的sql语句产生in()或values()的情况,而这种情况不符合SQL的语法,最终会导致bad SQL grammar []; nested exception is .的问题。

看到这个报错,大家肯定就会意识到是sql语句的问题,那么我们该如何解决这个问题呢?

网络上有一些现成的解决方案:

1、对list判null和判空来处理

<if test="list != null and >0">
    do something
</if>
复制代码

这种方案解决了sql语句有误的问题,但同时产生了一个新的逻辑问题。本来预想的in一个空列表,查询结果应该是没有数据才对,但实际上这么写会导致这个in条件失效,这就导致了执行结果并非不是我们想要的问题。

2、对list做双重判断。第一重判断和上面的解决方案一致,增加的第二重判断是为了保证如果list为空列表则只能查到空列表

<if test="list != null and >0">
    do something
</if>
<if test="list!=null and ==0">
    and 1=0
</if>
复制代码

这种方案能解决sql报错的问题,也不会产生逻辑错误的情况。但是这个写法有点繁琐,每次遇到这种情况都需要特殊判断,会极大降低开发的效率。

那么还有更优雅的写法么?

答案肯定是有的,我们可以通过拦截器Interceptor来优雅的解决这个问题。其他业务同学,还是和往常一样,只需要在xml中判断list非null就可以了。

@Intercepts({
        @Signature(type = , method = "query", args = {, , , }),
        @Signature(type = , method = "query", args = {, ,, , , }),
        @Signature(type = , method = "update", args = {,})})
public class EmptyCollectionIntercept implements Interceptor {
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        //通过()可以得到当前执行方法的参数
        //第一个args[0]是MappedStatement对象,第二个args[1]是参数对象parameterObject。
        final Object[] args = ();
        MappedStatement mappedStatement = (MappedStatement) args[0];
        Object parameter = args[1];
        if (parameter == null) {
            Class parameterType = ().getType();
            // 实际执行时的参数值为空,但mapper语句上存在输入参数的异常状况,返回默认值
            if (parameterType != null) {
                return getDefaultReturnValue(invocation);
            }
            return ();
        }
        BoundSql boundSql = (parameter);
        if (isHaveEmptyList(())) {
            return getDefaultReturnValue(invocation);
        }
        return ();
    }

    @Override
    public Object plugin(Object target) {
        //只拦截Executor对象,减少目标被代理的次数
        if (target instanceof Executor) {
            return (target, this);
        } else {
            return target;
        }
    }

    @Override
    public void setProperties(Properties properties) {

    }

    /**
     * 返回默认的值,list类型的返回空list,数值类型的返回0
     *
     * @param invocation
     * @return
     */
    private Object getDefaultReturnValue(Invocation invocation) {
        Class returnType = ().getReturnType();
        if (return()) {
            return ();
        } else if (return() || return()
                || return() || return()) {
            return 0;
        }
        return null;
    }

    /**
     * 去除字符中的干扰项,避免字符串中的内容干扰判断。
     *
     * @param sql
     * @return
     */
    private static String removeInterference(String sql) {
        Pattern pattern = ("[\"|'](.*?)[\"|']");
        Matcher matcher = (sql);
        while (()) {
            String replaceWorld = ();
            sql = (replaceWorld, "''");
        }
        return sql;
    }

    /**
     * 判断是否存在空list
     *
     * @param sql
     * @param methodName
     * @return
     */
    private static Boolean isHaveEmptyList(String sql, String methodName) {
        sql = removeInterference(sql);
        List<String> keyWorldList = ("in", "values");
        Boolean isHaveEmptyList = ;
        for (String keyWorld : keyWorldList) {
            List<Integer> indexList = ();
            //获取关键词后的index,关键词前必须为空白字符,但以关键词开头的单词也会被匹配到,例如index
            Pattern pattern = ("\\s(?i)" + keyWorld);
            Matcher matcher = (sql);
            while (()) {
                (());
            }
            if ((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<Integer> indexList) {
        Boolean isHaveEmptyList = ;
        //获取()内的内容
        Pattern p2 = ("(?<=\\()(.+?)(?=\\))");
        for (Integer index : indexList) {
            String subSql = (index);
            //如果关键词之后无任何sql语句,则sql语句结尾为关键词,此时判定为空列表
            if ((subSql)) {
                isHaveEmptyList = ;
                break;
            }
            //关键词后必须是(或者是空字符或者是换行符等才有继续判断的意义,避免sql中存在以关键词in或values开头的单词的情况干扰判断
            boolean flag = ("(")
                    || (" ")
                    || ("\n")
                    || ("\r");
            if (!flag) {
                continue;
            }
            subSql = ();
            //如果关键词后的sql语句trim后不以(开头,也判定为空列表
            if (!("(")) {
                isHaveEmptyList = ;
                break;
            }
            Matcher m2 = (subSql);
            //如果括号()内的内容trim后为空,则判定为空列表
            if (()) {
                if ((().trim())) {
                    isHaveEmptyList = ;
                    break;
                }
            }
        }
        return isHaveEmptyList;
    }
}
复制代码

具体的判断过程如上所示,关键代码已写注释,阅读起来应该不费事。

最后,把我们写的拦截器加入到sqlSessionFactory的plugins即可投入使用。

<property name="plugins">
    <array>
        <bean class="">
            <property name="properties">
                <value>property-key=property-value</value>
            </property>
        </bean>
    </array>
</property>
复制代码

关于mybatis的拦截器Interceptor,有兴趣的可以自行查阅一下。

这个插件在我司已使用了1年多,目前正常运作。如果大家在使用过程中有什么问题,欢迎留言联系。