单表增删改查的重复书写相当冗余,目前为了避免这样的冗余我们会使用通用mapper,但是当遇到表名动态变化的时候,比如按年、月、天分表就需要写常规的增删改查sql,这时候就会失去通用mapper单表不用写sql的优势。
此时可以使用通用Mapper动态拦截器操作表名。
@Getter
public enum TableEnum {
UNSERVICEDAY("t_mac_unservice_day", "未运营日报"),
SERVICEDAY("t_mac_service_day", "运营日报"),
;
private String table;
private String desc;
TableEnum(String table, String desc) {
this.table = table;
this.desc = desc;
}
public static TableEnum of(String value) {
Optional assetEventEnum = Arrays.stream(TableEnum.values())
.filter(c -> Objects.equals(c.getTable(),value)).findFirst();
return assetEventEnum.orElse(null);
}
}
@Configuration
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
DynamicTableNameInnerInterceptor dynamicTableNameInnerInterceptor = new DynamicTableNameInnerInterceptor();
dynamicTableNameInnerInterceptor.setTableNameHandler((sql, tableName) -> {
//表名操作
TableEnum tableEnum = TableEnum.of(tableName);
switch (tableEnum) {
case SERVICEDAY:
case UNSERVICEDAY:
return tableName + CommonConstant.SPLIT_CHAR_ + LocalDateTime.now().minusDays(CommonConstant.ONE).format(DateTimeUtil.YYYYMMDD_FORMATTER);
default:
return tableName;
}
});
//加入Mybatis的拦截器
interceptor.addInnerInterceptor(dynamicTableNameInnerInterceptor);
return interceptor;
}
}
虽然大部分的sql都是对当天的表进行操作,但是总有操作不是针对当天的,例如创建、删除表、查询过往数据。
初期走了一些弯路,本来是想使用@MapperScan({"***.domain.mapper"})限制这个拦截配置类的作用范围,将拦截限制在固定路径下,然后将不需要拦截的单独在其他路径下编写。但是这个拦截器是注册在Mybatis内部,底层还是使用Mybatis的拦截sql机制,所以限制作用范围是不起作用的,具体内容感兴趣的可以看原理分析。
回归正题,那么如果不拦截该sql呢?通过查阅通用Mapper的相关文档了解到有一个注解可以使用。对于通用Mapper提供的动态表名、行级租户等多种功能都可以进行忽略政策,加在Mapper层的方法上就可以避免拦截。
public @interface InterceptorIgnore {
/**
* 行级租户 {@link com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor}
*/
String tenantLine() default "";
/**
* 动态表名 {@link com.baomidou.mybatisplus.extension.plugins.inner.DynamicTableNameInnerInterceptor}
*/
String dynamicTableName() default "";
/**
* 攻击 SQL 阻断解析器,防止全表更新与删除 {@link com.baomidou.mybatisplus.extension.plugins.inner.BlockAttackInnerInterceptor}
*/
String blockAttack() default "";
/**
* 垃圾SQL拦截 {@link com.baomidou.mybatisplus.extension.plugins.inner.IllegalSQLInnerInterceptor}
*/
String illegalSql() default "";
/**
* 数据权限 {@link com.baomidou.mybatisplus.extension.plugins.inner.DataPermissionInterceptor}
*
* 默认关闭,需要注解打开
*/
String dataPermission() default "1";
/**
* 分表 {@link com.baomidou.mybatisplus.extension.plugins.inner.ShardingInnerInterceptor}
*/
String sharding() default "";
/**
* 其他的
*
* 格式应该为: "key"+"@"+可选项[false,true,1,0,on,off]
* 例如: "xxx@1" 或 "xxx@true" 或 "xxx@on"
*
* 如果配置了该属性的注解是注解在 Mapper 上的,则如果该 Mapper 的一部分 Method 需要取反则需要在 Method 上注解并配置此属性为反值
* 例如: "xxx@1" 在 Mapper 上, 则 Method 上需要 "xxx@0"
*/
String[] others() default {};
}
public interface MacUnserviceDayMapper extends BaseMapper {
/**
* 分页展示
* @param pageQuery
* @return
*/
List pageList(@Param("query") PageQueryRequest pageQuery);
List exportList(@Param("query") PageQueryRequest pageQuery);
/**
* 获取分页数量.
*
* @param pageQuery
* @return
*/
int pageCount(@Param("query")PageQueryRequest pageQuery);
//删除指定表
@InterceptorIgnore(dynamicTableName = "true")
int deleteBySelect(@Param("timeSuffix")String timeSuffix);
}
总体架构如下图
从下图可以看到Mybatis对于sql方法的拦截,动态表名等拦截器实际上只是注册到了它的局部变量interceptors中,所以在Mybatis统一的拦截机制下,给注册的拦截器设置作用范围也就不会生效了。
@Intercepts(
{
@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class}),
@Signature(type = StatementHandler.class, method = "getBoundSql", args = {}),
@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}),
@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 MybatisPlusInterceptor implements Interceptor {
@Setter
private List interceptors = new ArrayList<>();
其实只是加载到Mybatis的局部变量中
public void addInnerInterceptor(InnerInterceptor innerInterceptor) {
this.interceptors.add(innerInterceptor);
}
在intercept方法中将记载的拦截器进行遍历
public Object intercept(Invocation invocation) throws Throwable {
Object target = invocation.getTarget();
Object[] args = invocation.getArgs();
if (target instanceof Executor) {
final Executor executor = (Executor) target;
Object parameter = args[1];
boolean isUpdate = args.length == 2;
MappedStatement ms = (MappedStatement) args[0];
if (!isUpdate && ms.getSqlCommandType() == SqlCommandType.SELECT) {
RowBounds rowBounds = (RowBounds) args[2];
ResultHandler resultHandler = (ResultHandler) args[3];
BoundSql boundSql;
if (args.length == 4) {
boundSql = ms.getBoundSql(parameter);
} else {
boundSql = (BoundSql) args[5];
}
//遍历缓存的拦截器
for (InnerInterceptor query : interceptors) {
if (!query.willDoQuery(executor, ms, parameter, rowBounds, resultHandler, boundSql)) {
return Collections.emptyList();
}
//进入查询的前置方法
query.beforeQuery(executor, ms, parameter, rowBounds, resultHandler, boundSql);
}
CacheKey cacheKey = executor.createCacheKey(ms, parameter, rowBounds, boundSql);
return executor.query(ms, parameter, rowBounds, resultHandler, cacheKey, boundSql);
} else if (isUpdate) {
for (InnerInterceptor update : interceptors) {
if (!update.willDoUpdate(executor, ms, parameter)) {
return -1;
}
update.beforeUpdate(executor, ms, parameter);
}
}
} else {
// StatementHandler
final StatementHandler sh = (StatementHandler) target;
// 目前只有StatementHandler.getBoundSql方法args才为null
if (null == args) {
for (InnerInterceptor innerInterceptor : interceptors) {
innerInterceptor.beforeGetBoundSql(sh);
}
} else {
Connection connections = (Connection) args[0];
Integer transactionTimeout = (Integer) args[1];
for (InnerInterceptor innerInterceptor : interceptors) {
innerInterceptor.beforePrepare(sh, connections, transactionTimeout);
}
}
}
return invocation.proceed();
}
beforeQuery负责是否进行表解析的判断
public void beforeQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
PluginUtils.MPBoundSql mpBs = PluginUtils.mpBoundSql(boundSql);
//检测是否忽略sql
if (InterceptorIgnoreHelper.willIgnoreDynamicTableName(ms.getId())) return;
//进入sql解析
mpBs.sql(this.changeTable(mpBs.sql()));
}
根据INTERCEPTOR_IGNORE_CACHE中的缓存判断是否进入拦截方法
public static boolean willIgnore(String id, Function function) {
//获取sql方法对应的注解缓存
InterceptorIgnoreCache cache = INTERCEPTOR_IGNORE_CACHE.get(id);
if (cache == null) {
cache = INTERCEPTOR_IGNORE_CACHE.get(id.substring(0, id.lastIndexOf(StringPool.DOT)));
}
if (cache != null) {
//比较缓存检查的的属性,此处是dynamicTableName
Boolean apply = function.apply(cache);
return apply != null && apply;
}
return false;
}
sql解析,解析出表名进入业务方法。
protected String changeTable(String sql) {
ExceptionUtils.throwMpe(null == tableNameHandler, "Please implement TableNameHandler processing logic");
//拆分sql
TableNameParser parser = new TableNameParser(sql);
List names = new ArrayList<>();
// 表解析
parser.accept(names::add);
StringBuilder builder = new StringBuilder();
int last = 0;
for (TableNameParser.SqlToken name : names) {
int start = name.getStart();
if (start != last) {
builder.append(sql, last, start);
//进入业务方法
builder.append(tableNameHandler.dynamicTableName(sql, name.getValue()));
}
last = name.getEnd();
}
if (last != sql.length()) {
builder.append(sql.substring(last));
}
return builder.toString();
}
表解析,获取表名给使用者在业务方法中进行处理。
public void accept(TableNameVisitor visitor) {
int index = 0;
String first = tokens.get(index).getValue();
if (isOracleSpecialDelete(first, tokens, index)) {
//首字符串是删除,只支持紧跟表名
visitNameToken(tokens.get(index + 1), visitor);
} else if (isCreateIndex(first, tokens, index)) {
//首字符串是创建,只支持创建索引
visitNameToken(tokens.get(index + 4), visitor);
} else {
//遍历所有字符串
while (hasMoreTokens(tokens, index)) {
String current = tokens.get(index++).getValue();
if (isFromToken(current)) {
//找到from字符串
processFromToken(tokens, index, visitor);
} else if (isOnDuplicateKeyUpdate(current, index)) {
//找到duplicate字符串,后面是不是update字符串
index = skipDuplicateKeyUpdateIndex(index);
} else if (concerned.contains(current.toLowerCase())) {
// 找到table、into、join、using、update字符串,认为后续紧跟表名
if (hasMoreTokens(tokens, index)) {
SqlToken next = tokens.get(index++);
visitNameToken(next, visitor);
}
}
}
}
}
对于需要按照业务情况分表的情况有很对,对应的工具也有很多,本文主要是表过期就会进行删除,所以没有必要使用sharding的分区方案,采用了Mybatis的动态拦截。