Mybatis之通用Mapper動態(tài)表名及其原理分析
一、引言
單表增刪改查的重復書寫相當冗余,目前為了避免這樣的冗余我們會使用通用mapper,但是當遇到表名動態(tài)變化的時候,比如按年、月、天分表就需要寫常規(guī)的增刪改查sql,這時候就會失去通用mapper單表不用寫sql的優(yōu)勢。
此時可以使用通用Mapper動態(tài)攔截器操作表名。
二、使用
1、枚舉類
@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<TableEnum> assetEventEnum = Arrays.stream(TableEnum.values())
.filter(c -> Objects.equals(c.getTable(),value)).findFirst();
return assetEventEnum.orElse(null);
}
}2、攔截器
@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;
}
}3、部分sql不攔截
雖然大部分的sql都是對當天的表進行操作,但是總有操作不是針對當天的,例如創(chuàng)建、刪除表、查詢過往數(shù)據(jù)。
初期走了一些彎路,本來是想使用@MapperScan({"***.domain.mapper"})限制這個攔截配置類的作用范圍,將攔截限制在固定路徑下,然后將不需要攔截的單獨在其他路徑下編寫。
但是這個攔截器是注冊在Mybatis內(nèi)部,底層還是使用Mybatis的攔截sql機制,所以限制作用范圍是不起作用的,具體內(nèi)容感興趣的可以看原理分析。
回歸正題,那么如果不攔截該sql呢?通過查閱通用Mapper的相關文檔了解到有一個注解可以使用。
對于通用Mapper提供的動態(tài)表名、行級租戶等多種功能都可以進行忽略政策,加在Mapper層的方法上就可以避免攔截。
public @interface InterceptorIgnore {
/**
* 行級租戶 {@link com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor}
*/
String tenantLine() default "";
/**
* 動態(tài)表名 {@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 "";
/**
* 數(shù)據(jù)權限 {@link com.baomidou.mybatisplus.extension.plugins.inner.DataPermissionInterceptor}
* <p>
* 默認關閉,需要注解打開
*/
String dataPermission() default "1";
/**
* 分表 {@link com.baomidou.mybatisplus.extension.plugins.inner.ShardingInnerInterceptor}
*/
String sharding() default "";
/**
* 其他的
* <p>
* 格式應該為: "key"+"@"+可選項[false,true,1,0,on,off]
* 例如: "xxx@1" 或 "xxx@true" 或 "xxx@on"
* <p>
* 如果配置了該屬性的注解是注解在 Mapper 上的,則如果該 Mapper 的一部分 Method 需要取反則需要在 Method 上注解并配置此屬性為反值
* 例如: "xxx@1" 在 Mapper 上, 則 Method 上需要 "xxx@0"
*/
String[] others() default {};
}4、Mapper
public interface MacUnserviceDayMapper extends BaseMapper<MacUnserviceDayEntity> {
/**
* 分頁展示
* @param pageQuery
* @return
*/
List<MacUnserviceDayEntity> pageList(@Param("query") PageQueryRequest<MacDayRequestDTO> pageQuery);
List<MacUnserviceDayEntity> exportList(@Param("query") PageQueryRequest<MacDayRequestDTO> pageQuery);
/**
* 獲取分頁數(shù)量.
*
* @param pageQuery
* @return
*/
int pageCount(@Param("query")PageQueryRequest<MacDayRequestDTO> pageQuery);
//刪除指定表
@InterceptorIgnore(dynamicTableName = "true")
int deleteBySelect(@Param("timeSuffix")String timeSuffix);
}三、原理分析
總體架構如下圖

1、Mybatis攔截模式
從下圖可以看到Mybatis對于sql方法的攔截,動態(tài)表名等攔截器實際上只是注冊到了它的局部變量interceptors中,所以在Mybatis統(tǒng)一的攔截機制下,給注冊的攔截器設置作用范圍也就不會生效了。
@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<InnerInterceptor> interceptors = new ArrayList<>();2、動態(tài)表名攔截器注冊
其實只是加載到Mybatis的局部變量中
public void addInnerInterceptor(InnerInterceptor innerInterceptor) {
this.interceptors.add(innerInterceptor);
}3、攔截器生效
在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()));
}根據(jù)INTERCEPTOR_IGNORE_CACHE中的緩存判斷是否進入攔截方法
public static boolean willIgnore(String id, Function<InterceptorIgnoreCache, Boolean> 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解析,解析出表名進入業(yè)務方法。
protected String changeTable(String sql) {
ExceptionUtils.throwMpe(null == tableNameHandler, "Please implement TableNameHandler processing logic");
//拆分sql
TableNameParser parser = new TableNameParser(sql);
List<TableNameParser.SqlToken> 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);
//進入業(yè)務方法
builder.append(tableNameHandler.dynamicTableName(sql, name.getValue()));
}
last = name.getEnd();
}
if (last != sql.length()) {
builder.append(sql.substring(last));
}
return builder.toString();
}表解析,獲取表名給使用者在業(yè)務方法中進行處理。
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)) {
//首字符串是創(chuàng)建,只支持創(chuàng)建索引
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字符串,認為后續(xù)緊跟表名
if (hasMoreTokens(tokens, index)) {
SqlToken next = tokens.get(index++);
visitNameToken(next, visitor);
}
}
}
}
}四、總結
對于需要按照業(yè)務情況分表的情況有很對,對應的工具也有很多,本文主要是表過期就會進行刪除,所以沒有必要使用sharding的分區(qū)方案,采用了Mybatis的動態(tài)攔截。
以上為個人經(jīng)驗,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關文章
java中List去除重復數(shù)據(jù)的5種方式總結
這篇文章主要給大家總結介紹了關于java中List去除重復數(shù)據(jù)的5種方式,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2021-01-01
SpringBoot如何使用Scala進行開發(fā)的實現(xiàn)
這篇文章主要介紹了SpringBoot如何使用Scala進行開發(fā)的實現(xiàn),文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2019-12-12
Spring Task定時任務每天零點執(zhí)行一次的操作
這篇文章主要介紹了Spring Task定時任務每天零點執(zhí)行一次的操作,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-09-09
macOS中搭建Java8開發(fā)環(huán)境(基于Intel?x86?64-bit)
這篇文章主要介紹了macOS中搭建Java8開發(fā)環(huán)境(基于Intel?x86?64-bit)?的相關資料,需要的朋友可以參考下2022-12-12

