mybatis plus like 模糊检索时支持%_作为普通参数使用。

mybatis plus like 模糊检索时支持%_作为普通参数使用。

[TOC]

总结:

实现方式有多种:

1、通过mybatis plus 拦截器(com.baomidou.mybatisplus.extension.plugins.inner.InnerInterceptor)实现,只能不能处理第二步SQL执行。
2、通过mybatis 原生拦截器(org.apache.ibatis.plugin.Interceptor)实现,不能解决QueryWrapper的问题。
3、使用工具类SqlUtils.convertToSQLSafeValue 处理所有的like 传参。缺点是侵入代码。
4、spring 过滤器拦截特殊字符,通过自定义异常返回。此处不写。

都可以实现,根据各自的拦截器实现原理,在分页处理上两者会有明显不同。

mybatis plus分页处理,会执行两遍SQL查询:第一次执行SQL获取总数量, 第二次执行根据总数量进行分页查询。在只有第一步总数量有效的情况下会执行第二次查询。拦截器只作用在第二步上,关键代码如下:

com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor 代码 :

// com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor
// 所有拦截器
for (InnerInterceptor query : interceptors) {
    // 执行查询,确认有无结果
    if (!query.willDoQuery(executor, ms, parameter, rowBounds, resultHandler, boundSql)) {
        // 无结果不再执行query(mybatis plus 拦截器)
        return Collections.emptyList();
    }
    // 只有成功获取数量后,才会调用mybatis plus 拦截器。
    query.beforeQuery(executor, ms, parameter, rowBounds, resultHandler, boundSql);
}
CacheKey cacheKey = executor.createCacheKey(ms, parameter, rowBounds, boundSql);
// 执行SQL
return executor.query(ms, parameter, rowBounds, resultHandler, cacheKey, boundSql);

mybatis 同时可以作用在所有的SQL查询上,也就是上面myabtis plus 的分页两步执行,mybatis 拦截器都可以拦截。但是此种方式只能处理掉“?”占位符所使用的参数,并不能处理 QueryWrapper.like()相关接口传参,如果使用QueryWrapper传参需要使用mybatis的sql 使用方式。

先放两种实现方式的共用代码:

1、Mybatis 工具类

package com.commons.mybatis.utils;

import com.commons.utils.SqlUtils;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.ParameterMapping;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

@Slf4j
@NoArgsConstructor
public class MyBatisUtil {

    /**
     * 检查sql中,是否含有like查询
     */
    public static final Pattern LIKE_PARAM_PATTERN = Pattern.compile("like\\s(concat)?[\\w'\"\\(\\)\\%,\\s\\n\\t]*\\?", Pattern.CASE_INSENSITIVE);

    public static void escapeParameterIfContainingLike(BoundSql boundSql) {
        if (boundSql == null) {
            return;
        }
        String prepareSql = boundSql.getSql();
        List parameterMappings = boundSql.getParameterMappings();

        // 找到 like 后面的参数
        List position = findLikeParam(prepareSql);
        if (position.isEmpty()) {
            return;
        }

        List likeParameterMappings = new ArrayList<>(parameterMappings.size());

        // 复制
        MetaObject metaObject = SystemMetaObject.forObject(boundSql.getParameterObject());
        for (ParameterMapping m : parameterMappings) {
            if (!metaObject.hasGetter(m.getProperty())) {
                continue;
            }
            boundSql.setAdditionalParameter(m.getProperty(), metaObject.getValue(m.getProperty()));
        }

        for (int i = 0; i < position.size(); i++) {
            ParameterMapping parameterMapping = parameterMappings.get(position.get(i));
            likeParameterMappings.add(parameterMapping);
        }
        // 覆盖 转义字符
        delegateMetaParameterForEscape(boundSql, likeParameterMappings);
    }

    /**
     * @param boundSql 原 boundSql
     * @param likeParameterMappings 需要转义的参数
     * @return 支持转义的 boundSql
     */
    public static void delegateMetaParameterForEscape(
            BoundSql boundSql,
            List likeParameterMappings) {

        log.debug("like String Escape parsing ...");

        MetaObject metaObject = SystemMetaObject.forObject(boundSql.getParameterObject());

        for (ParameterMapping mapping : likeParameterMappings) {

            String property = mapping.getProperty();

            if (!metaObject.hasGetter(property)) {
                continue;
            }

            Object value = metaObject.getValue(property);
            if (value instanceof String) {
                boundSql.setAdditionalParameter(property, convertToSQLSafeValue((String) value));
            }
        }
    }

    /**
     * 匹配like 位置, 如
     * like concat('%',?,'%')
     * like CONCAT('%',?,'%')
     * LIKE CONCAT('%', ?,'%')
     * lIKE conCAT('%', ?,'%')
     * like ?
     * @param prepareSql
     * @return
     */
    public static List findLikeParam(String prepareSql) {

        if (StringUtils.isEmpty(prepareSql)) {
            return Collections.emptyList();
        }
        Matcher matcher = LIKE_PARAM_PATTERN.matcher(prepareSql);

        if (!matcher.find()) {
            return Collections.emptyList();
        }

        matcher.reset();
        int pos = 0;
        List indexes = new ArrayList<>();
        while (matcher.find(pos)) {
            int start = matcher.start();
            int index = StringUtils.countMatches(prepareSql.substring(0, start), "?");
            indexes.add(index);
            pos = matcher.end();
        }
        return indexes;
    }


    /**
     * MySQL需要转义的字段:\ % _
     */
    public static final Pattern PATTERN_MYSQL_ESCAPE_CHARS = Pattern.compile("(['_%\\\\]{1})");

    /**
     * 在SQL进行like时使用 ,mysql like时,参数使用传值 SqlUtils.convertToSQLSafeValue(String); 禁止与escape 同时使用。
     *
     * 转义mysql的特殊字符 包括 '\', '%', '_', ''',
     * @param str
     * @return 返回可能为null eg:
     *  1'2_3%4\ 5 ?\ 转义后  1\'2\_3\%4\\\\ 5 ?\\\\
     *  null >> null
     *  """ >> ""
     *  "%" >> "\%"
     *  "\" >> "\\\\\"
     *  "_" >> "\_"
     *  "_%" >> "\_\%"
     */
    public static String convertToSQLSafeValue(String str) {
        return SqlUtils.convertToSQLSafeValue(str);
    }
}

2、SqlUtils 工具类

package com.commons.utils.SqlUtils

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * SQL相关工具类
 * @creator DZ
 * @create 2021-08-25 17:25:52
 */
public class SqlUtils {

    private SqlUtils() {
    }

    private static final Logger logger = LoggerFactory.getLogger(SqlUtils.class);

    /**
     * MySQL需要转义的字段:\ % _
     */
    public static final Pattern PATTERN_MYSQL_ESCAPE_CHARS = Pattern.compile("(['_%\\\\]{1})");

    /**
     * 在SQL进行like时使用 ,mysql like时,参数使用传值 SqlUtils.convertToSQLSafeValue(String); 禁止与escape 同时使用。
     *
     * 转义mysql的特殊字符 包括 '\', '%', '_', ''',
     * @param str
     * @return 返回可能为null eg:
     *  1'2_3%4\ 5 ?\ 转义后  1\'2\_3\%4\\\\ 5 ?\\\\
     *  null >> null
     *  """ >> ""
     *  "%" >> "\%"
     *  "\" >> "\\\\\"
     *  "_" >> "\_"
     *  "_%" >> "\_\%"
     */
    public static String convertToSQLSafeValue(String str) {
        if (str == null) {
            return null;
        }
        Matcher matcher = PATTERN_MYSQL_ESCAPE_CHARS.matcher(str);
        int charSplitStart = 0;
        if (!matcher.find()) {
            return str;
        }
        StringBuilder sb = new StringBuilder();
        matcher.reset();
        while (matcher.find()) {
            String ch = str.substring(matcher.start(), matcher.end());
            sb.append(str, charSplitStart, matcher.start())
                    .append('\\').append("\\".equals(ch) ? "\\\\\\" : ch);
            charSplitStart = matcher.end();
        }
        if (sb.length() == 0) return str;
        String result = sb.toString();
        logger.debug("对SQL参数进行转义:{} => {} ", str, result);
        return result;
    }

}

mybatis plus 拦截器实现方式

1、添加like 转义拦截器

// mybatis plus config 
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 添加like 转义拦截器
interceptor.addInnerInterceptor(new LikeStringEscapeInterceptor()); 

2、mybatis plus 转义拦截器 : LikeStringEscapeInterceptor.java

需要实现com.baomidou.mybatisplus.extension.plugins.inner.InnerInterceptor 接口

import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.InnerInterceptor;
import com.commons.mybatis.utils.MyBatisUtil;
import lombok.NoArgsConstructor;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlCommandType;
import org.apache.ibatis.mapping.StatementType;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.sql.SQLException;

/**
 * Like 转义 插件:
* 在mybatis plus 配置此插件使用;mybatis plus 插件使用机制,优先使用原始参数进行条件查询。
* 1、如果 count 记录为0 时,name将不再执行任何before query 调用;
* 2、如果 count 结果非0 时,执行插件业务逻辑。
* * @create dz * @date 2021-08-26 16:34:59 * @see MybatisPlusInterceptor#intercept(org.apache.ibatis.plugin.Invocation) * for (InnerInterceptor query : interceptors) { * if (!query.willDoQuery(executor, ms, parameter, rowBounds, resultHandler, boundSql)) { * return Collections.emptyList(); * } * query.beforeQuery(executor, ms, parameter, rowBounds, resultHandler, boundSql); * } *

* 使用方法: *

    *
  1. 添加插件 到mybatis plus
  2. *
*

*/ @NoArgsConstructor public class LikeStringEscapeInterceptor implements InnerInterceptor { private static final Logger logger = LoggerFactory.getLogger(LikeStringEscapeInterceptor.class); @Override public void beforeQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException { // 到此,说明mybatis plus 预执行有结果。 logger.debug("LikeStringEscapeInterceptor beforeQuery"); SqlCommandType sqlCommandType = ms.getSqlCommandType(); StatementType statementType = ms.getStatementType(); // 只处理 有参数的查询语句 if (sqlCommandType == SqlCommandType.SELECT && statementType == StatementType.PREPARED) { MyBatisUtil.escapeParameterIfContainingLike(boundSql); } } }

3、结果

3.1. 数据库中有【默认】开始的数据。设定参数【默认%】分页两步骤SQL,只有第二步执行了转义。

stPage_mpCount : ==>  Preparing: SELECT COUNT(1) FROM (SELECT r
stPage_mpCount : ==> Parameters: 默认%(String)
stPage_mpCount : <==      Total: 1
peInterceptor  : LikeStringEscapeInterceptor beforeQuery
peInterceptor  : like String Escape parsing ...
ils.SqlUtils   : 对SQL参数进行转义:默认% => 默认\% 
peInterceptor  : boundSql AdditionalParameter property: param.n
ListPage       : ==>  Preparing: SELECT role.id, role.name, rol
ListPage       : ==> Parameters: 默认\%(String), 10(Long)

3.2. 数据库无【九月天】开头的数据,不再执行插件逻辑。设定参数【九月天】,只有第一步计数SQL ,无转义后SQL

Param_mpCount    : ==>  Preparing: SELECT COUNT(1) FROM 
Param_mpCount    : ==> Parameters: 九月天%(String)
Param_mpCount    : <==      Total: 1 

mybatis 原生拦截器实现

1、LikeStringEscapeInterceptorForMybatis.java

实现com.commons.mybatis.interceptor.LikeStringEscapeInterceptorForMybatis代码

package com.commons.mybatis.interceptor;

import com.commons.mybatis.utils.MyBatisUtil;
import org.apache.ibatis.cache.CacheKey;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlCommandType;
import org.apache.ibatis.mapping.StatementType;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

/**
 * Like 转义
 * 

* 使用方法: *

    *
  1. 添加插件
  2. *
*

*/ @Component @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}), } ) public class LikeStringEscapeInterceptorForMybatis implements Interceptor { private static final Logger logger = LoggerFactory.getLogger(LikeStringEscapeInterceptorForMybatis.class); @Override public Object plugin(Object target) { return Plugin.wrap(target, this); } @Override public Object intercept(Invocation invocation) throws Throwable { Object[] args = invocation.getArgs(); MappedStatement ms = (MappedStatement) args[0]; Object parameter = args[1]; RowBounds rowBounds = (RowBounds) args[2]; ResultHandler resultHandler = (ResultHandler) args[3]; Executor executor = (Executor) invocation.getTarget(); CacheKey cacheKey; BoundSql boundSql; //由于逻辑关系,只会进入一次 if (args.length == 4) { //4 个参数时 boundSql = ms.getBoundSql(parameter); cacheKey = executor.createCacheKey(ms, parameter, rowBounds, boundSql); } else { //6 个参数时 cacheKey = (CacheKey) args[4]; boundSql = (BoundSql) args[5]; } SqlCommandType sqlCommandType = ms.getSqlCommandType(); StatementType statementType = ms.getStatementType(); // 只处理 有参数的查询语句 if (sqlCommandType == SqlCommandType.SELECT && statementType == StatementType.PREPARED) { MyBatisUtil.escapeParameterIfContainingLike(boundSql); return executor.query(ms, parameter, rowBounds, resultHandler, cacheKey, boundSql); } return invocation.proceed(); } }

2、结果:

2.1. 查询条件:【九月天%】,数据无【九月天】开始的数据。mybatis plus 只查询了一次,执行了转义。OK

MyBatisUtil  : like String Escape parsing ...
s.SqlUtils   : 对SQL参数进行转义:九月天% => 九月天\% 
Dept_mpCount : ==>  Preparing: SELECT COUNT(1) FROM (SELECT DIST
Dept_mpCount : ==> Parameters: 001002031(String), 九月天\%(String)
Dept_mpCount : <==      Total: 1

2.2. 查询条件:【g%h】 ,数据中有含有“g%h”的文本数据。mybatisplus执行了分页处理:两步都执行了转义操作。OK

ils.MyBatisUtil  : like String Escape parsing ...
utils.SqlUtils   : 对SQL参数进行转义:g%h => g\% 
geInDept_mpCount : ==>  Preparing: SELECT COUNT(1) FROM (SELECT D
geInDept_mpCount : ==> Parameters: 001002031(String), g\%(String)
geInDept_mpCount : <==      Total: 1
ils.MyBatisUtil  : like String Escape parsing ...
utils.SqlUtils   : 对SQL参数进行转义:g%h => g\% 
ageInDept        : ==>  Preparing: select u.id, u.username, u.rea
ageInDept        : ==> Parameters: 001002031(String), g\%(String)
ageInDept        : <==      Total: 1

2.3 使用注意

qw.like(SysUserEntity::getUsername, "%"); 

// 日志如下传入
// .SqlUtils : 对SQL参数进行转义:%%% => %%%
// lectList : ==> Preparing: SELECT id,username
// lectList : ==> Parameters: %%%(String)

queryWrapper.apply("`" + column_name + "` LIKE concat('%',{0})", value);

此方式OK

参考:
Mybatis 拦截器实现 Like 通配符转义

你可能感兴趣的:(mybatis plus like 模糊检索时支持%_作为普通参数使用。)