当前位置: 首页 > backend >正文

Mysql中模糊匹配常被忽略的坑

我们先来看看问题现象

准备测试数据库的数据

这里会使用下面sql进行模糊匹配查询

SELECT * FROM product WHERE product_name like '%#{productName}%'

实际执行的sql

返回结果如下

可以看到这里跟我们预期的结果并不一样,我们预期应该只返回一个数据:20%第二组数据

但是这里将我们的测试数据全部都返回了。

原因是在MySQL模糊查询中,%_需要转义的原因在于它们是 SQL 标准中 LIKE操作符的通配符,具有特殊含义,不能直接作为普通字符进行匹配。

这个时候就需要使用转义符'\',而转义符本身也是需要转义的。

解决这个的办法有两种。

1,使用util方法,在需要处理的地方调用方法进行处理(常见)

2,使用mysql的拦截器,拦截sql的like查询,修改实际的执行sql(方便)

这里我就只展示第二种,第一种想必大家都知道怎么做

拦截器修改sql(这里还有存量的打印sql信息的内容)

package com.luojie.config.myInterface.mybatisIntercept;import com.luojie.common.Conditions;
import com.luojie.util.TxtUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.ObjectUtils;
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.ParameterMapping;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
import java.util.Properties;
import java.util.regex.Matcher;/*** 自定义打印日志功能的拦截器*/
@Intercepts({// 拦截 Executor 接口的 query 方法,包含不同的参数组合@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}),@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),@Signature(type = Executor.class, method = "queryCursor", args = {MappedStatement.class, Object.class, RowBounds.class}),@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class})
})
@Slf4j
public class SqlPrintInterceptor implements Interceptor {private static final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");@Overridepublic Object intercept(Invocation invocation) throws Throwable {// 记录开始时间long startTime = System.currentTimeMillis();Object proceed = null;// 执行原始方法try {// 获取执行的SQL语句,判断是否是模糊查询MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0];Object parameter = invocation.getArgs().length > 1 ? invocation.getArgs()[1] : null;BoundSql boundSql = mappedStatement.getBoundSql(parameter);String sql = boundSql.getSql();// 如果是模糊查询,在执行SQL之前修改参数if (isLikeQuery(sql)) {modifyParametersForLikeQuery(invocation);// 重新获取参数和BoundSql,因为参数可能被修改parameter = invocation.getArgs().length > 1 ? invocation.getArgs()[1] : null;boundSql = mappedStatement.getBoundSql(parameter);}proceed = invocation.proceed();} catch (Throwable t) {log.error("Error during SQL execution", t);throw t; // 重新抛出异常}// 记录结束时间long endTime = System.currentTimeMillis();long executionTime = endTime - startTime; // 计算执行时间// 转换执行时间为 "XXs.XXms" 格式String formattedExecutionTime = formatExecutionTime(executionTime);// 生成打印的 SQL 语句String printSql = generateSql(invocation);// 输出 SQL 和执行时间System.out.println(Conditions.RED + "SQL: " + printSql);System.out.println("Execution time: " + formattedExecutionTime);System.out.print(Conditions.RESET);log.info("SQL: " + printSql);log.info("Execution time: " + formattedExecutionTime);// 记录慢sql(这里我为了方便观察,所以设置界限为0,各位可以根据实际情况设置)if ((executionTime / 1000) >= 0) {writeSlowSqlToLocation(printSql, formattedExecutionTime);}return proceed; // 返回原始方法的结果}// 记录慢sqlprivate void writeSlowSqlToLocation(String sql, String executeTime) {String formattedDate = dateFormat.format(new Date());String logs = formattedDate + "  SQL: " + sql + "  执行耗时: " + executeTime;TxtUtil.writeLog(logs);}// 新增格式化执行时间的方法private String formatExecutionTime(long executionTime) {long seconds = executionTime / 1000; // 获取秒数long milliseconds = executionTime % 1000; // 获取剩余的毫秒数return String.format("%ds.%03dms", seconds, milliseconds); // 格式化为 "XXs.XXXms"}/*** 生成用于日志显示的SQL语句* 注意:此方法只用于日志显示,不影响实际执行的SQL*/private String generateSql(Invocation invocation) {// 获取 MappedStatement 对象MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0];Object parameter = null;// 获取参数对象if (invocation.getArgs().length > 1) {parameter = invocation.getArgs()[1];}// 获取 MyBatis 配置Configuration configuration = mappedStatement.getConfiguration();// 获取 BoundSql 对象BoundSql boundSql = mappedStatement.getBoundSql(parameter);// 获取参数对象Object parameterObject = boundSql.getParameterObject();// 获取参数映射列表List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();// 获取执行的 SQL 语句String sql = boundSql.getSql();// 替换 SQL 中多个空格为一个空格sql = sql.replaceAll("[\\s]+", " ");// 情况1: 有参数对象且有参数映射if (!ObjectUtils.isEmpty(parameterObject) && !ObjectUtils.isEmpty(parameterMappings)) {// 如果只有一个参数,直接替换if (parameterObject instanceof String && parameterMappings.size() == 1) {return sql.replaceFirst("\\?", String.valueOf(parameterObject)); // 处理缺少值的情况}// 遍历每个参数映射for (ParameterMapping parameterMapping : parameterMappings) {String propertyName = parameterMapping.getProperty(); // 获取属性名MetaObject metaObject = configuration.newMetaObject(parameterObject); // 创建 MetaObjectObject obj = null; // 初始化参数对象// 如果参数对象有对应的 getter 方法if (metaObject.hasGetter(propertyName)) {obj = metaObject.getValue(propertyName); // 获取参数值} else if (boundSql.hasAdditionalParameter(propertyName)) {obj = boundSql.getAdditionalParameter(propertyName); // 获取附加参数}// 替换 SQL 中的占位符if (obj != null) {sql = sql.replaceFirst("\\?", Matcher.quoteReplacement(getParameterValue(obj)));} else {sql = sql.replaceFirst("\\?", Matcher.quoteReplacement(propertyName)); // 处理缺少值的情况}}}// 情况2: 只有参数对象,没有参数映射 (通常是单个简单参数)else if (!ObjectUtils.isEmpty(parameterObject) && ObjectUtils.isEmpty(parameterMappings)) {// 其他情况直接替换sql = sql.replaceFirst("\\?", Matcher.quoteReplacement(getParameterValue(parameterObject)));}// 情况3: 有参数映射但没有参数对象else if (ObjectUtils.isEmpty(parameterObject) && !ObjectUtils.isEmpty(parameterMappings)) {// 遍历每个参数映射for (ParameterMapping parameterMapping : parameterMappings) {String propertyName = parameterMapping.getProperty(); // 获取属性名// 尝试从附加参数中获取值Object obj = null;if (boundSql.hasAdditionalParameter(propertyName)) {obj = boundSql.getAdditionalParameter(propertyName); // 获取附加参数}// 替换 SQL 中的占位符if (obj != null) {sql = sql.replaceFirst("\\?", Matcher.quoteReplacement(getParameterValue(obj)));} else {sql = sql.replaceFirst("\\?", Matcher.quoteReplacement(propertyName)); // 处理缺少值的情况}}}return sql; // 返回生成的 SQL 语句}/*** 修改模糊查询的参数值,对特殊字符进行转义* 注意:此方法会修改实际执行的SQL参数*/private void modifyParametersForLikeQuery(Invocation invocation) throws Exception {// 获取 MappedStatement 对象MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0];Object parameter = null;// 获取参数对象if (invocation.getArgs().length > 1) {parameter = invocation.getArgs()[1];}// 获取 MyBatis 配置Configuration configuration = mappedStatement.getConfiguration();// 获取 BoundSql 对象BoundSql boundSql = mappedStatement.getBoundSql(parameter);// 获取参数对象Object parameterObject = boundSql.getParameterObject();// 获取参数映射列表List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();// 情况1: 有参数对象且有参数映射if (!ObjectUtils.isEmpty(parameterObject) && !ObjectUtils.isEmpty(parameterMappings)) {// 遍历每个参数映射for (ParameterMapping parameterMapping : parameterMappings) {String propertyName = parameterMapping.getProperty(); // 获取属性名MetaObject metaObject = configuration.newMetaObject(parameterObject); // 创建 MetaObject// 如果参数对象有对应的 getter 方法且是字符串类型,进行转义if (metaObject.hasGetter(propertyName)) {Object obj = metaObject.getValue(propertyName); // 获取参数值if (obj instanceof String) {String escapedValue = escapeSqlSpecialChars((String) obj);metaObject.setValue(propertyName, escapedValue);}}}}// 情况2: 只有参数对象,没有参数映射 (通常是单个简单参数)else if (!ObjectUtils.isEmpty(parameterObject) && parameterObject instanceof String) {// 直接替换参数对象String escapedValue = escapeSqlSpecialChars((String) parameterObject);invocation.getArgs()[1] = escapedValue;}// 情况3: 检查是否有附加参数if (!ObjectUtils.isEmpty(parameterMappings)) {for (ParameterMapping parameterMapping : parameterMappings) {String propertyName = parameterMapping.getProperty(); // 获取属性名if (boundSql.hasAdditionalParameter(propertyName)) {Object obj = boundSql.getAdditionalParameter(propertyName); // 获取附加参数if (obj instanceof String) {String escapedValue = escapeSqlSpecialChars((String) obj);// 注意:MyBatis的BoundSql不支持直接修改附加参数// 这里我们记录日志,但不进行实际修改log.debug("Found additional parameter {} that should be escaped, but cannot modify BoundSql additional parameters", propertyName);}}}}}private String getParameterValue(Object parameterObject) {// 如果参数对象为空,返回 "null"if (parameterObject == null) {return "null";}// 返回参数对象的字符串表示return parameterObject.toString();}/*** 转义SQL中的特殊字符* @param value 需要转义的值* @return 转义后的值*/private String escapeSqlSpecialChars(String value) {if (value == null) {return null;}// 转义 % _ 通配符和 \ 转义符return value.replace("\\", "\\\\").replace("%", "\\%").replace("_", "\\_");}/*** 检查SQL是否是模糊查询语句* @param sql SQL语句* @return 是否是模糊查询*/private boolean isLikeQuery(String sql) {if (sql == null) {return false;}// 忽略大小写检查SQL中是否包含LIKE关键字return sql.toLowerCase().contains(" like ");}@Overridepublic Object plugin(Object target) {// 生成插件对象return Interceptor.super.plugin(target);}@Overridepublic void setProperties(Properties properties) {// 设置属性Interceptor.super.setProperties(properties);}
}

测试:

http://www.xdnf.cn/news/20135.html

相关文章:

  • Netty从0到1系列之Netty整体架构、入门程序
  • Python迭代协议完全指南:从基础到高并发系统实现
  • 投资储能项目能赚多少钱?小程序帮你测算
  • Unity2022.3.41的TargetSdk更新到APILevel 35问题
  • Fairness, bias, and ethics|公平,偏见与伦理
  • 【科研绘图系列】R语言绘制论文合集图
  • 高等数学知识补充:三角函数
  • 脚本语言的大浪淘沙或百花争艳
  • JUnit入门:Java单元测试全解析
  • Boost搜索引擎 查找并去重(3)
  • 输入网址到网页显示的整个过程
  • 孙宇晨钱包被列入黑名单,WLFI代币价格暴跌引发中心化争议
  • Unix/Linux 平台通过 IP 地址获取接口名的 C++ 实现
  • 告别 “无效阅读”!2025 开学季超赞科技书单,带孩子解锁 AI、编程新技能
  • Docker部署PanSou 一款开源网盘搜索项目,集成前后端,一键部署
  • 基于单片机汽车防撞系统设计
  • validator列表校验
  • OCA、OCP、OCM傻傻分不清?Oracle认证就看这篇
  • 四六级学习资料管理系统的设计与实现(代码+数据库+LW)
  • pandas的使用(1)0905
  • 如何制造一个AI Agent:从“人工智障”到“人工智能”的奇幻漂流
  • leetcode399.除法求值
  • Redis-持久化
  • 疯狂星期四文案网第61天运营日记
  • CSP-J初赛for(auto)用法
  • 【Leetcode】高频SQL基础题--180.连续出现的数字
  • 计算机原理-计算机操作系统-硬盘缓存、断电丢数据篇
  • 力扣416:分割等和子集
  • 【无GGuF版本】如何在Colab下T4运行gpt-oss 20B
  • spring事物失效场景