我们使用MyBatis-Plus执行LIKE模糊查询时,若预处理参数包含_ % \等字符(欢迎补充),会查询出所有结果,这不是我们需要的。

不论写法是自定义SQL

xxx like concat('%',#{fuzzyName},'%')

还是Wrapper(本质上也是生成like SQL语句)

final LambdaQueryWrapper<XxxPo> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.like(CharSequenceUtil.isNotBlank(fuzzyName), XxxPo::getName, fuzzyName);

因为SQL中LIKE中_ % \这些符号是通配符,若要作为正常参数查询需要转译。

\转译为\\
_转译为\_
%转译为\%

1、每处like查询替换特殊字符(不推荐)

照前文所述,我们只需定义一个替换方法,在调用like的地方把参数处理一下。

    /*** 转译 \ % _* 禁止与escape 同时使用*/public static String convertToSqlSafeValue(String str) {if (CharSequenceUtil.isEmpty(str)) {return str;}return str.replace("\\", "\\\\").replace("%", "\\%").replace("_", "\\_");}

但是,这种做法有诸多缺点:

  1. 侵入性太强,需要每处like参数进行处理,而且有些参数在对象内,可能会改变属性值

  1. 业务庞杂的系统,修改容易遗漏,且下次写like时容易忘记加这个方法,项目交接也不易

  1. 太不优雅了,写代码不应这样

  1. 若公司有多个项目,每个项目每个like都需要写这个东西,建议使用下面的方法,并集成进公司/项目组的基础组件中

2、自定义插件解决普通查询的like特殊字符问题

MyBatis-Plus的核心插件MybatisPlusInterceptor代理了Executor#query和Executor#update和 StatementHandler#prepare方法。

允许我们通过实现InnerInterceptor接口,创建MybatisPlusInterceptor对象,注入Bean中生效。以MyBatis-Plus提供的扩展“分页插件PaginationInnerInterceptor”为例:

    @Beanpublic MybatisPlusInterceptor paginationInterceptor() {final MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();interceptor.addInnerInterceptor(new PaginationInnerInterceptor());return interceptor;}

阅读MybatisPlusInterceptor的源码可知,当执行SELECT的SQL时,会执行各InnerInterceptor实现类的beforeQuery方法(同理UPDATE时会执行beforeUpdate方法),源码如下截图:

因此,创建一个类实现InnerInterceptor接口,在beforeQuery中将“_%\”等特殊字符做转译替换。我将其命名为LikeStringEscapeInterceptor,读者可自行命名。

import com.baomidou.mybatisplus.extension.plugins.inner.InnerInterceptor;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;import java.sql.SQLException;/*** Like 转义插件:* 在MyBatis-Plus配置此插件使用;MyBatis-Plus插件使用机制,优先使用原始参数进行条件查询。* 1、如果 count 记录为0 时,name将不再执行任何before query 调用;* 2、如果 count 结果非0 时,执行插件业务逻辑。** @author 陨·落* @date 2023/1/31 14:49*/
public class LikeStringEscapeInterceptor implements InnerInterceptor {@Overridepublic void beforeQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds,ResultHandler resultHandler, BoundSql boundSql) throws SQLException {// 为了在分页插件中复用,此处抽取出静态方法MybatisUtil.escapeParameterIfContainingLike(ms, boundSql);InnerInterceptor.super.beforeQuery(executor, ms, parameter, rowBounds, resultHandler, boundSql);}}

为了后文中分页插件中也复用这个like特殊字符替换方法,将具体实现抽取出静态方法MybatisUtil#escapeParameterIfContainingLike。其中加一层判断只处理有参数的查询语句,以跳过其他没必要处理的SQL,并通过正则表达式定位like concat('%',?,'%')所在位置。

import cn.hutool.core.text.CharSequenceUtil;
import org.apache.ibatis.mapping.*;
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.Locale;
import java.util.regex.Matcher;
import java.util.regex.Pattern;/*** @author 陨·落* @date 2023/1/31 14:55*/
public class MybatisUtil {private 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(MappedStatement ms, BoundSql boundSql) {final SqlCommandType sqlCommandType = ms.getSqlCommandType();final StatementType statementType = ms.getStatementType();// 只处理 有参数的查询语句if (SqlCommandType.SELECT == sqlCommandType && StatementType.PREPARED == statementType) {escapeParameterIfContainingLike(boundSql);}}public static void escapeParameterIfContainingLike(BoundSql boundSql) {if (null == boundSql) {return;}final String prepareSql = boundSql.getSql();final List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();// 找到 like 后面的参数final List<Integer> positions = findLikeParam(prepareSql);if (positions.isEmpty()) {return;}final List<ParameterMapping> likeParameterMappings = new ArrayList<>(parameterMappings.size());// 复制final MetaObject metaObject = SystemMetaObject.forObject(boundSql.getParameterObject());for (ParameterMapping parameterMapping : parameterMappings) {final String property = parameterMapping.getProperty();if (!metaObject.hasGetter(property)) {continue;}boundSql.setAdditionalParameter(property, metaObject.getValue(property));}for (Integer position : positions) {final ParameterMapping parameterMapping = parameterMappings.get(position);likeParameterMappings.add(parameterMapping);}// 覆盖 转译字符delegateMetaParameterForEscape(boundSql, likeParameterMappings);}/*** @param boundSql              原BoundSql* @param likeParameterMappings 需要转义的参数*/private static void delegateMetaParameterForEscape(BoundSql boundSql,List<ParameterMapping> likeParameterMappings) {final MetaObject metaObject = SystemMetaObject.forObject(boundSql.getParameterObject());for (ParameterMapping mapping : likeParameterMappings) {final String property = mapping.getProperty();if (!metaObject.hasGetter(property)) {continue;}final Object value = metaObject.getValue(property);if (value instanceof String) {final String saveValue = convertToSqlSafeValue((String) value);boundSql.setAdditionalParameter(property, saveValue);}}}/*** 匹配like 位置, 如* like concat('%',?,'%')* like CONCAT('%',?,'%')* LIKE CONCAT('%', ?,'%')* lIKE conCAT('%', ?,'%')* like ?*/private static List<Integer> findLikeParam(String prepareSql) {if (CharSequenceUtil.isBlank(prepareSql)) {return Collections.emptyList();}final Matcher matcher = LIKE_PARAM_PATTERN.matcher(prepareSql);if (!matcher.find()) {return Collections.emptyList();}matcher.reset();int pos = 0;final List<Integer> indexes = new ArrayList<>();while (matcher.find(pos)) {final int start = matcher.start();final int index = CharSequenceUtil.count(prepareSql.substring(0, start), '?');indexes.add(index);pos = matcher.end();}return indexes;}/*** 转译 \ % _* 禁止与escape 同时使用*/public static String convertToSqlSafeValue(String str) {if (CharSequenceUtil.isEmpty(str)) {return str;}return str.replace("\\", "\\\\").replace("%", "\\%").replace("_", "\\_");}}

实现写完后,记得在Bean中应用MyBatis-Plus插件。

import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;/*** @author 陨·落* @date 2023/2/17 15:00*/
@Configuration
@RequiredArgsConstructor
public class MybatisPlusConfig {@Beanpublic MybatisPlusInterceptor paginationInterceptor() {final MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();interceptor.addInnerInterceptor(new LikeStringEscapeInterceptor());return interceptor;}}

3、自定义插件解决分页查询的like特殊字符问题

使用MyBatis-Plus的分页功能,通常离不开官方插件PaginationInnerInterceptor的支持。

PaginationInnerInterceptor的主要功能有两个:

  1. 统计总数

  1. 拼接SQL实现分页查询

阅读源码,可知统计数量count是在PaginationInnerInterceptor插件willDoQuery方法完成的。而count结果若为0,willDoQuery方法返回false,就不会执行具体的查询方法(因为查询范围内条数都为0了没必要)。

若我们不重写willDoQuery方法,当count遇到特殊字符,返回条数为0时会直接结束查询,这不是我们想要的结果,我们是想特殊字符作为查询条件 进行分页查询。

因此,我们继承PaginationInnerInterceptor类,重写其willDoQuery方法,方法内容与官方大体不变,只需加一句替换countSql的特殊字符进行转译。

import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
import com.baomidou.mybatisplus.core.toolkit.ParameterUtils;
import com.baomidou.mybatisplus.core.toolkit.PluginUtils;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
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.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;import java.sql.SQLException;
import java.util.List;/*** 在 分页拦截器 的基础上,分页计数sql查询条件like 处理\ % _等特殊字符** @author 陨·落* @date 2023/1/31 18:13*/
public class PaginationInnerEscapeInterceptor extends PaginationInnerInterceptor {@Overridepublic boolean willDoQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {IPage<?> page = ParameterUtils.findPage(parameter).orElse(null);if (page == null || page.getSize() < 0 || !page.searchCount()) {return true;}BoundSql countSql;MappedStatement countMs = buildCountMappedStatement(ms, page.countId());if (countMs != null) {countSql = countMs.getBoundSql(parameter);} else {countMs = buildAutoCountMappedStatement(ms);String countSqlStr = autoCountSql(page.optimizeCountSql(), boundSql.getSql());PluginUtils.MPBoundSql mpBoundSql = PluginUtils.mpBoundSql(boundSql);countSql = new BoundSql(countMs.getConfiguration(), countSqlStr, mpBoundSql.parameterMappings(), parameter);PluginUtils.setAdditionalParameter(countSql, mpBoundSql.additionalParameters());}CacheKey cacheKey = executor.createCacheKey(countMs, parameter, rowBounds, countSql);// 其余和官方一致,只需加这句:处理like条件\ % _等特殊字符MybatisUtil.escapeParameterIfContainingLike(countMs, countSql);List<Object> result = executor.query(countMs, parameter, rowBounds, resultHandler, cacheKey, countSql);long total = 0;if (CollectionUtils.isNotEmpty(result)) {// 个别数据库 count 没数据不会返回 0Object o = result.get(0);if (o != null) {total = Long.parseLong(o.toString());}}page.setTotal(total);return continuePage(page);}}

然后记得在Bean中使用重写的对象,而非官方的。

import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;/*** @author 陨·落* @date 2023/2/17 15:00*/
@Configuration
@RequiredArgsConstructor
public class MybatisPlusConfig {@Beanpublic MybatisPlusInterceptor paginationInterceptor() {final MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();interceptor.addInnerInterceptor(new LikeStringEscapeInterceptor());interceptor.addInnerInterceptor(new PaginationInnerEscapeInterceptor());return interceptor;}}

至此,分页查询的count查询与主体查询,like concat('%',?,'%')处 预处理参数均经过转译处理,问题解决。

只需启动程序,以往模糊查询遇_ % \问题均被解决。

自定义插件解决MyBatis-Plus like查询遇_ % \等字符需转译问题(含分页查询)相关推荐

  1. long mode 分页_在Spring Boot中使用Spring-data-jpa实现分页查询(转)

    在我们平时的工作中,查询列表在我们的系统中基本随处可见,那么我们如何使用jpa进行多条件查询以及查询列表分页呢?下面我将介绍两种多条件查询方式. 1.引入起步依赖 org.springframewor ...

  2. python查询最高分_精通 Oracle+Python,第 1 部分:查询最佳应践

    作者:Przemyslaw Piotrowski 首先,熟悉 Oracle-Python 连接性的基本概念 2007 年 9 月发布 参见系列目录 在 Python 做事方式的核心原则中,有一个规定是 ...

  3. python查询最高分_精通 Oracle+Python,第 1 部分:查询最佳应践

    在 Python 做事方式的核心原则中,有一个规定是要求具有到 API 的高级接口.数据库 API(在此例中为 Oracle API)就是一个例子.使用 Computronix 的 cx_Oracle ...

  4. 查询去重_【Freya的MySQL课堂】DQL基础查询

    MYSQL 基础查询 各位小伙伴们晚上好,今天是10月22号. 我是你们的Freya. 今天我们开始学习MySQL中的DQL语言. Do Not Stop Learning 我 的 小 课 堂 我爱学 ...

  5. mysql分页总页数算法解析_详解MySQL的limit用法和分页查询语句的性能分析

    limit用法 在我们使用查询语句的时候,经常要返回前几条或者中间某几行数据,这个时候怎么办呢?不用担心,mysql已经为我们提供了这样一个功能. SELECT * FROM table LIMIT ...

  6. oracle分页查询中的page,用简单的例子解释Oracle分页查询

    什么是分页查询 分页查询就是把query到的结果集按页显示.比如一个结果集有1W行,每页按100条数据库.而你获取了第2页的结果集. 为什么要分页查询 如果一个结果集有几十W行,那么在一个页面上显示肯 ...

  7. 根据城市的三字代码查询经纬度_百度地图API简单应用——1.根据地址查询经纬度...

    这几天比较空闲,就接触了下百度地图的API(开发者中心链接地址:http://developer.baidu.com),发现调用还是挺方便的.只要简单几步注册下,就可以获得一个Key,就能直接调用(P ...

  8. oraclf 复杂查询练习_刷完这些烧脑的SQL练习题,复杂查询才能熟能生巧

    练习题:SQLZOO world: SELECT within SELECT Tutorial SELECT within SELECT Tutorial/zh​sqlzoo.net 1.List e ...

  9. mysql两表查询单个_对两个表进行单个MySQL选择查询是可能的吗?

    是的,有可能.以下是语法-select * from yourTableName1,yourTableName2; 让我们首先创建一个表-mysql> create table DemoTabl ...

最新文章

  1. java opcode 反汇编,OPCode详解及汇编与反汇编原理
  2. Linux下使用Vim粘贴文本错乱问题解决
  3. 1-1-2 交叉编译工具链
  4. 计算器的程序代码java_java 简单的计算器程序实例代码
  5. 数据库专家Michael Stonebraker获得2014年图灵奖
  6. Java http方式提交短信到短信网关
  7. 【FZU - 2140 】Forever 0.5 (计算几何,构造)
  8. 筛选出1-n之间的个位数字为1的素数(Java代码实现)
  9. python自定义异常捕获_python怎么自定义捕获错误
  10. php通过数组存取mysql查询语句的返回值
  11. 2013应届毕业生“人人网”校招应聘总结
  12. linux7启动ib子网管理器,IB_Switch交换机SB7890配置及Band网卡
  13. ant design——Modal
  14. CentOS导入CA证书
  15. 集训队作业2018: 喂鸽子(min-max容斥)
  16. websocket握手失败_WebSocket通信之握手协议
  17. 如何用计算机做大爆炸模拟,法国完成首个宇宙结构计算机模型 模拟大爆炸至今...
  18. UNCTF2020web方向部分题解
  19. Elasticsearch数据库all shards failed
  20. Java菜鸟逆袭之基础语法下

热门文章

  1. RGBA 编码为 YUV420SP【NEON】
  2. 云场景实践研究第40期:网聚宝
  3. 用大白话说说JavaWeb相关技术
  4. 中台渐入佳境,云徙科技的有所为与有所不为
  5. 【论文阅读笔记】Autoencoder as Assistant Supervisor
  6. mmdetection3d SUN RGB-D数据集预处理
  7. 一元购抽奖号码 thinkphp php
  8. 学习图神经网络相关内容
  9. os.environ[‘CUDA_VISIBLE_DEVICES‘]指定GPU后,还是用的“0“卡
  10. unity VR实现相机完美旋转