Mybatis系列:解決foreach標籤內list為空的問題

逍遙jc發表於2019-01-02

我把之前釋出在簡書的兩篇文章通過攔截器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<String> keyWorldList = Lists.newArrayList("in", "values");
        Boolean isHaveEmptyList = Boolean.FALSE;
        for (String keyWorld : keyWorldList) {
            List<Integer> 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<Integer> 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即可投入使用。

<property name="plugins">
    <array>
        <bean class="com.yibao.broker.intercepts.listIntercept.MyBatisCheckEmptyBeforeExecuteInterceptor">
            <property name="properties">
                <value>property-key=property-value</value>
            </property>
        </bean>
    </array>
</property>
複製程式碼

關於mybatis的攔截器Interceptor,有興趣的可以自行查閱一下。

這個外掛在我司已使用了1年多,目前正常運作。如果大家在使用過程中有什麼問題,歡迎留言聯絡。

相關文章