mybatis plus like 模糊檢索時(shí)支持%_作為普通參數(shù)使用丰捷。
[TOC]
總結(jié):
實(shí)現(xiàn)方式有多種:
1坯墨、通過(guò)mybatis plus 攔截器(com.baomidou.mybatisplus.extension.plugins.inner.InnerInterceptor)實(shí)現(xiàn),只能不能處理第二步SQL執(zhí)行瓢阴。
2畅蹂、通過(guò)mybatis 原生攔截器(org.apache.ibatis.plugin.Interceptor)實(shí)現(xiàn),不能解決QueryWrapper的問(wèn)題荣恐。
3、使用工具類(lèi)SqlUtils.convertToSQLSafeValue 處理所有的like 傳參累贤。缺點(diǎn)是侵入代碼叠穆。
4、spring 過(guò)濾器攔截特殊字符臼膏,通過(guò)自定義異常返回硼被。此處不寫(xiě)。
都可以實(shí)現(xiàn)渗磅,根據(jù)各自的攔截器實(shí)現(xiàn)原理嚷硫,在分頁(yè)處理上兩者會(huì)有明顯不同。
mybatis plus分頁(yè)處理始鱼,會(huì)執(zhí)行兩遍SQL查詢(xún):第一次執(zhí)行SQL獲取總數(shù)量仔掸, 第二次執(zhí)行根據(jù)總數(shù)量進(jìn)行分頁(yè)查詢(xún)。在只有第一步總數(shù)量有效的情況下會(huì)執(zhí)行第二次查詢(xún)医清。攔截器只作用在第二步上起暮,關(guān)鍵代碼如下:
com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor 代碼 :
// com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor
// 所有攔截器
for (InnerInterceptor query : interceptors) {
// 執(zhí)行查詢(xún),確認(rèn)有無(wú)結(jié)果
if (!query.willDoQuery(executor, ms, parameter, rowBounds, resultHandler, boundSql)) {
// 無(wú)結(jié)果不再執(zhí)行query(mybatis plus 攔截器)
return Collections.emptyList();
}
// 只有成功獲取數(shù)量后会烙,才會(huì)調(diào)用mybatis plus 攔截器负懦。
query.beforeQuery(executor, ms, parameter, rowBounds, resultHandler, boundSql);
}
CacheKey cacheKey = executor.createCacheKey(ms, parameter, rowBounds, boundSql);
// 執(zhí)行SQL
return executor.query(ms, parameter, rowBounds, resultHandler, cacheKey, boundSql);
mybatis 同時(shí)可以作用在所有的SQL查詢(xún)上,也就是上面myabtis plus 的分頁(yè)兩步執(zhí)行柏腻,mybatis 攔截器都可以攔截纸厉。但是此種方式只能處理掉“?”占位符所使用的參數(shù),并不能處理 QueryWrapper.like()相關(guān)接口傳參五嫂,如果使用QueryWrapper傳參需要使用mybatis的sql 使用方式颗品。
先放兩種實(shí)現(xiàn)方式的共用代碼:
1、Mybatis 工具類(lèi)
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中贫导,是否含有l(wèi)ike查詢(xún)
*/
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<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
// 找到 like 后面的參數(shù)
List<Integer> position = findLikeParam(prepareSql);
if (position.isEmpty()) {
return;
}
List<ParameterMapping> likeParameterMappings = new ArrayList<>(parameterMappings.size());
// 復(fù)制
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);
}
// 覆蓋 轉(zhuǎn)義字符
delegateMetaParameterForEscape(boundSql, likeParameterMappings);
}
/**
* @param boundSql 原 boundSql
* @param likeParameterMappings 需要轉(zhuǎn)義的參數(shù)
* @return 支持轉(zhuǎn)義的 boundSql
*/
public static void delegateMetaParameterForEscape(
BoundSql boundSql,
List<ParameterMapping> 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<Integer> 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<Integer> 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需要轉(zhuǎn)義的字段:\ % _
*/
public static final Pattern PATTERN_MYSQL_ESCAPE_CHARS = Pattern.compile("(['_%\\\\]{1})");
/**
* 在SQL進(jìn)行l(wèi)ike時(shí)使用 ,mysql like時(shí)孩灯,參數(shù)使用傳值 SqlUtils.convertToSQLSafeValue(String)闺金; 禁止與escape 同時(shí)使用。
*
* 轉(zhuǎn)義mysql的特殊字符 包括 '\', '%', '_', ''',
* @param str
* @return 返回可能為null eg:
* 1'2_3%4\ 5 ?\ 轉(zhuǎn)義后 1\'2\_3\%4\\\\ 5 ?\\\\
* null >> null
* """ >> ""
* "%" >> "\%"
* "\" >> "\\\\\"
* "_" >> "\_"
* "_%" >> "\_\%"
*/
public static String convertToSQLSafeValue(String str) {
return SqlUtils.convertToSQLSafeValue(str);
}
}
2峰档、SqlUtils 工具類(lèi)
package com.commons.utils.SqlUtils
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* SQL相關(guān)工具類(lèi)
* @creator DZ
* @create 2021-08-25 17:25:52
*/
public class SqlUtils {
private SqlUtils() {
}
private static final Logger logger = LoggerFactory.getLogger(SqlUtils.class);
/**
* MySQL需要轉(zhuǎn)義的字段:\ % _
*/
public static final Pattern PATTERN_MYSQL_ESCAPE_CHARS = Pattern.compile("(['_%\\\\]{1})");
/**
* 在SQL進(jìn)行l(wèi)ike時(shí)使用 败匹,mysql like時(shí)寨昙,參數(shù)使用傳值 SqlUtils.convertToSQLSafeValue(String); 禁止與escape 同時(shí)使用掀亩。
*
* 轉(zhuǎn)義mysql的特殊字符 包括 '\', '%', '_', ''',
* @param str
* @return 返回可能為null eg:
* 1'2_3%4\ 5 ?\ 轉(zhuǎn)義后 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("對(duì)SQL參數(shù)進(jìn)行轉(zhuǎn)義:{} => {} ", str, result);
return result;
}
}
mybatis plus 攔截器實(shí)現(xiàn)方式
1舔哪、添加like 轉(zhuǎn)義攔截器
// mybatis plus config
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 添加like 轉(zhuǎn)義攔截器
interceptor.addInnerInterceptor(new LikeStringEscapeInterceptor());
2、mybatis plus 轉(zhuǎn)義攔截器 : LikeStringEscapeInterceptor.java
需要實(shí)現(xiàn)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 轉(zhuǎn)義 插件:<br/>
* 在mybatis plus 配置此插件使用槽棍;mybatis plus 插件使用機(jī)制捉蚤,優(yōu)先使用原始參數(shù)進(jìn)行條件查詢(xún)。<br/>
* 1炼七、如果 count 記錄為0 時(shí)缆巧,name將不再執(zhí)行任何before query 調(diào)用; <br/>
* 2豌拙、如果 count 結(jié)果非0 時(shí)陕悬,執(zhí)行插件業(yè)務(wù)邏輯。 <br/>
*
* @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);
* }
* <p>
* 使用方法:
* <ol>
* <li> 添加插件 到mybatis plus </li>
* </ol>
* </p>
*/
@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 {
// 到此按傅,說(shuō)明mybatis plus 預(yù)執(zhí)行有結(jié)果捉超。
logger.debug("LikeStringEscapeInterceptor beforeQuery");
SqlCommandType sqlCommandType = ms.getSqlCommandType();
StatementType statementType = ms.getStatementType();
// 只處理 有參數(shù)的查詢(xún)語(yǔ)句
if (sqlCommandType == SqlCommandType.SELECT && statementType == StatementType.PREPARED) {
MyBatisUtil.escapeParameterIfContainingLike(boundSql);
}
}
}
3、結(jié)果
3.1. 數(shù)據(jù)庫(kù)中有【默認(rèn)】開(kāi)始的數(shù)據(jù)唯绍。設(shè)定參數(shù)【默認(rèn)%】分頁(yè)兩步驟SQL拼岳,只有第二步執(zhí)行了轉(zhuǎn)義。
stPage_mpCount : ==> Preparing: SELECT COUNT(1) FROM (SELECT r
stPage_mpCount : ==> Parameters: 默認(rèn)%(String)
stPage_mpCount : <== Total: 1
peInterceptor : LikeStringEscapeInterceptor beforeQuery
peInterceptor : like String Escape parsing ...
ils.SqlUtils : 對(duì)SQL參數(shù)進(jìn)行轉(zhuǎn)義:默認(rèn)% => 默認(rèn)\%
peInterceptor : boundSql AdditionalParameter property: param.n
ListPage : ==> Preparing: SELECT role.id, role.name, rol
ListPage : ==> Parameters: 默認(rèn)\%(String), 10(Long)
3.2. 數(shù)據(jù)庫(kù)無(wú)【九月天】開(kāi)頭的數(shù)據(jù)推捐,不再執(zhí)行插件邏輯裂问。設(shè)定參數(shù)【九月天】,只有第一步計(jì)數(shù)SQL 牛柒,無(wú)轉(zhuǎn)義后SQL
Param_mpCount : ==> Preparing: SELECT COUNT(1) FROM
Param_mpCount : ==> Parameters: 九月天%(String)
Param_mpCount : <== Total: 1
mybatis 原生攔截器實(shí)現(xiàn)
1堪簿、LikeStringEscapeInterceptorForMybatis.java
實(shí)現(xiàn)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 轉(zhuǎn)義
* <p>
* 使用方法:
* <ol>
* <li> 添加插件 </li>
* </ol>
* </p>
*/
@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;
//由于邏輯關(guān)系,只會(huì)進(jìn)入一次
if (args.length == 4) {
//4 個(gè)參數(shù)時(shí)
boundSql = ms.getBoundSql(parameter);
cacheKey = executor.createCacheKey(ms, parameter, rowBounds, boundSql);
} else {
//6 個(gè)參數(shù)時(shí)
cacheKey = (CacheKey) args[4];
boundSql = (BoundSql) args[5];
}
SqlCommandType sqlCommandType = ms.getSqlCommandType();
StatementType statementType = ms.getStatementType();
// 只處理 有參數(shù)的查詢(xún)語(yǔ)句
if (sqlCommandType == SqlCommandType.SELECT
&& statementType == StatementType.PREPARED) {
MyBatisUtil.escapeParameterIfContainingLike(boundSql);
return executor.query(ms, parameter, rowBounds, resultHandler, cacheKey, boundSql);
}
return invocation.proceed();
}
}
2皮壁、結(jié)果:
2.1. 查詢(xún)條件:【九月天%】椭更,數(shù)據(jù)無(wú)【九月天】開(kāi)始的數(shù)據(jù)。mybatis plus 只查詢(xún)了一次蛾魄,執(zhí)行了轉(zhuǎn)義虑瀑。OK
MyBatisUtil : like String Escape parsing ...
s.SqlUtils : 對(duì)SQL參數(shù)進(jìn)行轉(zhuǎn)義:九月天% => 九月天\%
Dept_mpCount : ==> Preparing: SELECT COUNT(1) FROM (SELECT DIST
Dept_mpCount : ==> Parameters: 001002031(String), 九月天\%(String)
Dept_mpCount : <== Total: 1
2.2. 查詢(xún)條件:【g%h】 ,數(shù)據(jù)中有含有“g%h”的文本數(shù)據(jù)滴须。mybatisplus執(zhí)行了分頁(yè)處理:兩步都執(zhí)行了轉(zhuǎn)義操作舌狗。OK
ils.MyBatisUtil : like String Escape parsing ...
utils.SqlUtils : 對(duì)SQL參數(shù)進(jìn)行轉(zhuǎn)義: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 : 對(duì)SQL參數(shù)進(jìn)行轉(zhuǎn)義: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 : 對(duì)SQL參數(shù)進(jìn)行轉(zhuǎn)義:%%% => %%%
// lectList : ==> Preparing: SELECT id,username
// lectList : ==> Parameters: %%%(String)
queryWrapper.apply("`" + column_name + "` LIKE concat('%',{0})", value);
此方式OK