🌟概述
在实际业务中,并不是所有表都需要做分表处理。因此我们结合自定义注解与 MyBatis 拦截器机制,优雅地实现了按需分表查询的功能。
本篇文章聚焦于实现逻辑,不深入探讨 MyBatis-plus 的底层机制,重点介绍如何在使用 LambdaQueryWrapper
查询时,实现自动分表。
👉 业务背景:本例场景仅需将数据划分为“今日表”和“历史表”。如果你有更多维度的分表需求,可在此基础上扩展 SQL 替换逻辑。
🎯 完整代码是1,2,4章节,第3章主要是分步详细解析,有基础的可以直接看完整代码,对疑惑的点在看详细的解析
🌟介绍
🎯 目标
当查询命中某一条件(如时间早于今日),自动将 SQL 中的表名替换为指定的历史表名,实现数据的逻辑分表查询。
🔄 实现流程
整个逻辑分为四个核心步骤:
使用 自定义注解 标记需要支持分表的实体类;
启动时或通过定时任务自动 创建历史表并迁移数据;
拦截 SQL 执行前,判断是否 命中历史表查询条件;
若命中,动态替换表名,执行分表查询。
1、自定义注解
主要有,分表后缀,分表字段。
注:因为我的业务需求只需要分一个历史数据表。
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @author 崔
* @Date:2025/04/26 14:18
* @vesion: 0.0.1
*/
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface TableSplit {
boolean split() default true;
//分表的后缀
String tableSuffix() default "";
//分表字段,根据哪个字段分,查询的时候如果有这个字段的条件,就会走对应的逻辑
String fieldName() default "create_date_time";
}
📌 说明:本业务场景只做了一个“历史表”拆分,字段默认为
create_date_time
,你可以按需修改。
2、mybatis-plus拦截器
我们通过实现 MyBatis-plus 的 Interceptor
接口,对 SQL 进行预处理和后处理。
1、
@Intercepts
注解,就是启用mybatis监听2、@Signature 注解就是说,我是查询/修改/新增哪些节点去才拦截
3、SqlListenerHandel 自定义类,里面有2个方法,前置处理,后置处理
实现流程,通过mybatis-plus的注解,进入拦截类,然后自己写了一个工具类,在里面处理他的sql,最后执行
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.cache.CacheKey;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
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.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import java.sql.Connection;
/**
* SQL拆分监听器拦截器
* 用于拦截MyBatis执行过程中的SQL语句,并在执行前后进行自定义处理
* 主要拦截 StatementHandler.prepare、Executor.query、Executor.update 三种方法
*
* @author 崔
* @date 2025/04/26
* @version 0.0.1
*/
@Setter
@Slf4j
@Intercepts({
@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.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 SqlListenerInterceptor implements Interceptor {
/**
* SQL监听处理器,用于自定义前置/后置处理逻辑
*/
private SqlListenerHandel sqlListenerHandel;
@Override
public Object intercept(Invocation invocation) throws Throwable {
long startTime = System.currentTimeMillis(); // 记录执行开始时间
// 调用前置处理方法,分表
sqlListenerHandel.beforeHandel(invocation);
String sql = ""; // 初始化SQL变量
// 根据拦截的方法不同,提取对应的SQL语句
if (invocation.getMethod().getName().equals("prepare")) {
// 拦截的是StatementHandler.prepare方法
StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
BoundSql boundSql = statementHandler.getBoundSql(); // 获取绑定的SQL对象
sql = boundSql.getSql(); // 提取SQL语句
} else if (invocation.getMethod().getName().equals("query")) {
// 拦截的是Executor.query方法
MappedStatement ms = (MappedStatement) invocation.getArgs()[0];
Object parameter = invocation.getArgs()[1];
sql = ms.getBoundSql(parameter).getSql(); // 提取SQL语句
} else if (invocation.getMethod().getName().equals("update")) {
// 拦截的是Executor.update方法
MappedStatement ms = (MappedStatement) invocation.getArgs()[0];
Object parameter = invocation.getArgs()[1];
sql = ms.getBoundSql(parameter).getSql(); // 提取SQL语句
}
// 执行原方法(即执行真实的SQL操作)
Object result = invocation.proceed();
// 调用后置处理方法(可以在这里做SQL执行结果分析、日志等)
sqlListenerHandel.afterHandel(invocation);
long endTime = System.currentTimeMillis(); // 记录执行结束时间
// 打印SQL执行时间
log.info(" ==> execTime: " + (endTime - startTime) + " ms");
// 如果SQL执行时间超过5秒,记录成错误日志,便于排查慢SQL
if ((endTime - startTime) > 5000) {
log.error("sql: " + sql + "; 耗时" + (endTime - startTime) + " ms");
}
return result; // 返回执行结果
}
}
3、核心处理逻辑
3.1 拦截逻辑判断
-
只拦截 SQL 准备阶段(
prepare
方法)。 -
通过反射获取 SQL 类型:
SELECT
/INSERT
/UPDATE
/DELETE
。 -
当前只处理
SELECT
语句。
public void beforeHandel(Invocation invocation) {
if (!invocation.getMethod().getName().equals("prepare")) {
return;
}
StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
MetaObject metaObject = SystemMetaObject.forObject(statementHandler);
String typeName = String.valueOf(metaObject.getValue("h.target.delegate.parameterHandler.sqlCommandType.name"));
if ("SELECT".equals(typeName)) {
selectBeforeHandel(statementHandler);
} else if ("INSERT".equals(typeName)) {
}
}
3.2 分表 SQL 替换逻辑
🧠 功能总结:
当查询的实体类标注了
@TableSplit
注解,并且查询条件中包含指定的时间字段,且时间早于今天,就会自动将 SQL 中的表名替换为历史表名
🔍 分步骤解析:
-
通过反射拿到查询实体类类型
MetaObject metaObject = SystemMetaObject.forObject(statementHandler); MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("h.target.delegate.mappedStatement"); Class<?> c = mappedStatement.getResultMaps().get(0).getType();
用于获取查询的实体类,从而通过注解获取到其他信息
-
判断是否需要分表
TableSplit tableSplit = (TableSplit) c.getAnnotation(TableSplit.class); if (tableSplit == null) { return; } f (StringUtil.isEmpty(tableName) || tableName.contains(tableSplit.tableSuffix())) { return; }
如果类上没有
@TableSplit
注解,说明当前表不支持分表,直接返回。 -
参数合法性校验
Object o = statementHandler.getParameterHandler().getParameterObject(); if (!(o instanceof MapperMethod.ParamMap)) {//查询 return; } MapperMethod.ParamMap<Object> parameterObject = (MapperMethod.ParamMap<Object>) o; if (!(parameterObject.containsKey("ew") && parameterObject.get("ew") instanceof LambdaQueryWrapper)) { return; }
仅支持使用 LambdaQueryWrapper 进行构建的查询(通常是 MP 的写法)。
-
是否命中历史分表的条件
//查询条件 boolean splitFlag = splitFlag(lambdaQueryWrapper, tableSplit); if (!splitFlag) { return; }
分析 SQL 中的查询字段和时间值,判断是否应该使用历史表。
-
执行 SQL 替换表名
String sql = statementHandler.getBoundSql().getSql(); String newSql = sql.replace(tableName, tableName + tableSplit.tableSuffix()); metaObject.setValue("h.target.delegate.boundSql.sql", newSql);
如果满足分表条件,将 SQL 中的表名替换为带后缀的新表名,实际执行时就会访问历史表。
3.3 条件匹配逻辑(是否切换历史表)
这个方法的作用是:根据查询条件中的字段和值判断,当前查询是否应该切换到历史表。
🚦 核心判断逻辑:
只有在查询中包含了
@TableSplit
注解指定的字段(通常是时间字段),并且该时间早于今天,才认为是对历史数据的查询。
🔍 分步骤解析
1. 获取条件表达式相关信息
Map<String, Object> paramMap = lambdaQueryWrapper.getParamNameValuePairs();
String sqlSegment = lambdaQueryWrapper.getExpression().getSqlSegment();
-
paramMap
:封装了所有查询条件中用到的字段和值。字段是被从新加载的。 -
sqlSegment
:条件的原始 SQL 片段,例如:date < #{ew.paramNameValuePairs.date}
。最后面的和上面paramMap对应
2. 匹配字段与值的对应关系
//获取本次包含的条件(= != AND IN ....)
final String normalStr = lambdaQueryWrapper.getExpression().getNormal().stream().filter(item -> item instanceof SqlKeyword).map(item -> item.getSqlSegment()).distinct().collect(Collectors.joining(""));
final Pattern pattern = Pattern.compile("(\\w+)\\s*[" + normalStr + "]+\\s*#\\{ew\\.paramNameValuePairs\\.(\\w+)\\}");
final Pattern patternParam = Pattern.compile("\\s*[" + normalStr + "]+\\s*");
final Matcher matcher = pattern.matcher(sqlSegment);
使用正则匹配从 SQL 中提取,就可以获取到每一个查询条件对应的值
对于相同的key,获取最小值,因我我的需求是,如果查了历史的,就不能查今日。做了拆分
这些信息会被用于后续判断
3.识别是否命中历史表条件
if (mappingValueMap.containsKey(tableSplit.fieldName()) && ((DateUtils.getDate("yyyy-MM-dd") + " 00:00:00").compareTo(mappingValueMap.get(tableSplit.fieldName()).toString()) > 0 || mappingSegmentMap.containsKey(tableSplit.fieldName()))) {
return true;
}
最后根据条件判断是否查询历史的表
4、完整代码
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.enums.SqlKeyword;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import org.apache.ibatis.binding.MapperMethod;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
/**
* sql拆分监听器,看查询哪个表
*
* @author 崔
* @Date:2024/11/2 12:39
* @vesion: 0.0.1
*/
@Component
public class SqlListenerHandel {
private static final Logger log = LoggerFactory.getLogger(SqlListenerHandel.class);
//前置拦截
public void beforeHandel(Invocation invocation) {
//不是执行前的结束
if (!invocation.getMethod().getName().equals("prepare")) {
return;
}
//通过反射获取到当前sql类型,SELECT/INSERT....
StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
MetaObject metaObject = SystemMetaObject.forObject(statementHandler);
String typeName = String.valueOf(metaObject.getValue("h.target.delegate.parameterHandler.sqlCommandType.name"));
if ("SELECT".equals(typeName)) {
selectBeforeHandel(statementHandler);
} else if ("INSERT".equals(typeName)) {
}
}
/**
* 查询拦截器
*
* @param statementHandler
*/
public void selectBeforeHandel(StatementHandler statementHandler) {
//通过反射获取到查询的实体类
MetaObject metaObject = SystemMetaObject.forObject(statementHandler);
MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("h.target.delegate.mappedStatement");
Class<?> c = mappedStatement.getResultMaps().get(0).getType();
String tableName = CreateUtil.getTableName(c);
//当前实体类是否有规定注解
TableSplit tableSplit = (TableSplit) c.getAnnotation(TableSplit.class);
if (tableSplit == null) {
return;
}
if (StringUtil.isEmpty(tableName) || tableName.contains(tableSplit.tableSuffix())) {
return;
}
//是否是查询
Object o = statementHandler.getParameterHandler().getParameterObject();
if (!(o instanceof MapperMethod.ParamMap)) {//查询
return;
}
//获取查询条件k和v
MapperMethod.ParamMap<Object> parameterObject = (MapperMethod.ParamMap<Object>) o;
if (!(parameterObject.containsKey("ew") && parameterObject.get("ew") instanceof LambdaQueryWrapper)) {
return;
}
final LambdaQueryWrapper<?> lambdaQueryWrapper = (LambdaQueryWrapper<?>) parameterObject.get("ew");
//查询条件 根据条件判断是否要修改查询的表名
boolean splitFlag = splitFlag(lambdaQueryWrapper, tableSplit);
if (!splitFlag) {
return;
}
//修改掉查询的表名,完成拦截
String sql = statementHandler.getBoundSql().getSql();
String newSql = sql.replace(tableName, tableName + tableSplit.tableSuffix());
metaObject.setValue("h.target.delegate.boundSql.sql", newSql);
}
/**
* 判断符不符合查询历史表的要求
*
* @param lambdaQueryWrapper
* @return
*/
public boolean splitFlag(final LambdaQueryWrapper<?> lambdaQueryWrapper, TableSplit tableSplit) {
try {
final Map<String, Object> mappingValueMap = Maps.newHashMap();
final Map<String, Object> mappingSegmentMap = Maps.newHashMap();
//获取到查询条件
final Map<String, Object> paramMap = lambdaQueryWrapper.getParamNameValuePairs();
final String sqlSegment = lambdaQueryWrapper.getExpression().getSqlSegment();
//获取本次包含的条件(= != AND IN ....)
final String normalStr = lambdaQueryWrapper.getExpression().getNormal().stream().filter(item -> item instanceof SqlKeyword).map(item -> item.getSqlSegment()).distinct().collect(Collectors.joining(""));
final Pattern pattern = Pattern.compile("(\\w+)\\s*[" + normalStr + "]+\\s*#\\{ew\\.paramNameValuePairs\\.(\\w+)\\}");
final Pattern patternParam = Pattern.compile("\\s*[" + normalStr + "]+\\s*");
final Matcher matcher = pattern.matcher(sqlSegment);
List<String> les = Lists.newArrayList("<","<=");
while (matcher.find()) {
String sqlMatch = matcher.group(0);
String key = matcher.group(1); // 提取键
String valueKey = matcher.group(2);
Object value = paramMap.get(valueKey); // 提取值
//相同的key获取值比较小的,比如时间,获取最小的时间
if (mappingValueMap.containsKey(key) && mappingValueMap.get(key).toString().compareTo(String.valueOf(value)) < 0) {
continue;
}
Matcher matcherParam = patternParam.matcher(sqlMatch);
while (matcherParam.find()) {
String segment = matcherParam.group(0).trim();
if (les.contains(segment)) {
mappingSegmentMap.put(key, segment);
}
}
mappingValueMap.put(key, value);
}
//判断规则的key是否满足条件
if (mappingValueMap.containsKey(tableSplit.fieldName())
&& ((DateUtils.getDate("yyyy-MM-dd") + " 00:00:00").compareTo(mappingValueMap.get(tableSplit.fieldName()).toString()) > 0
|| mappingSegmentMap.containsKey(tableSplit.fieldName()))
) {
return true;
}
} catch (Exception e) {
log.error("分表判断异常", e);
return false;
}
return false;
}
}
✅总结
-
本方案通过 MyBatis 拦截器机制实现 SQL 的动态分表,逻辑清晰、扩展性强;
-
目前支持
LambdaQueryWrapper
查询方式,其他写法可按需扩展; -
若你的业务需要更复杂的分表维度,只需拓展
splitFlag()
和replaceTableName()
方法逻辑即可。