Mybatis之通用Mapper動(dòng)態(tài)表名及其原理分析
一、引言
單表增刪改查的重復(fù)書寫相當(dāng)冗余,目前為了避免這樣的冗余我們會(huì)使用通用mapper,但是當(dāng)遇到表名動(dòng)態(tài)變化的時(shí)候,比如按年、月、天分表就需要寫常規(guī)的增刪改查sql,這時(shí)候就會(huì)失去通用mapper單表不用寫sql的優(yōu)勢。
此時(shí)可以使用通用Mapper動(dòng)態(tài)攔截器操作表名。
二、使用
1、枚舉類
@Getter
public enum TableEnum {
UNSERVICEDAY("t_mac_unservice_day", "未運(yùn)營日?qǐng)?bào)"),
SERVICEDAY("t_mac_service_day", "運(yùn)營日?qǐng)?bào)"),
;
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都是對(duì)當(dāng)天的表進(jìn)行操作,但是總有操作不是針對(duì)當(dāng)天的,例如創(chuàng)建、刪除表、查詢過往數(shù)據(jù)。
初期走了一些彎路,本來是想使用@MapperScan({"***.domain.mapper"})限制這個(gè)攔截配置類的作用范圍,將攔截限制在固定路徑下,然后將不需要攔截的單獨(dú)在其他路徑下編寫。
但是這個(gè)攔截器是注冊在Mybatis內(nèi)部,底層還是使用Mybatis的攔截sql機(jī)制,所以限制作用范圍是不起作用的,具體內(nèi)容感興趣的可以看原理分析。
回歸正題,那么如果不攔截該sql呢?通過查閱通用Mapper的相關(guān)文檔了解到有一個(gè)注解可以使用。
對(duì)于通用Mapper提供的動(dòng)態(tài)表名、行級(jí)租戶等多種功能都可以進(jìn)行忽略政策,加在Mapper層的方法上就可以避免攔截。
public @interface InterceptorIgnore {
/**
* 行級(jí)租戶 {@link com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor}
*/
String tenantLine() default "";
/**
* 動(dòng)態(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ù)權(quán)限 {@link com.baomidou.mybatisplus.extension.plugins.inner.DataPermissionInterceptor}
* <p>
* 默認(rèn)關(guān)閉,需要注解打開
*/
String dataPermission() default "1";
/**
* 分表 {@link com.baomidou.mybatisplus.extension.plugins.inner.ShardingInnerInterceptor}
*/
String sharding() default "";
/**
* 其他的
* <p>
* 格式應(yīng)該為: "key"+"@"+可選項(xiàng)[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);
}三、原理分析
總體架構(gòu)如下圖

1、Mybatis攔截模式
從下圖可以看到Mybatis對(duì)于sql方法的攔截,動(dòng)態(tài)表名等攔截器實(shí)際上只是注冊到了它的局部變量interceptors中,所以在Mybatis統(tǒng)一的攔截機(jī)制下,給注冊的攔截器設(shè)置作用范圍也就不會(huì)生效了。
@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、動(dòng)態(tài)表名攔截器注冊
其實(shí)只是加載到Mybatis的局部變量中
public void addInnerInterceptor(InnerInterceptor innerInterceptor) {
this.interceptors.add(innerInterceptor);
}3、攔截器生效
在intercept方法中將記載的攔截器進(jìn)行遍歷
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();
}
//進(jìn)入查詢的前置方法
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負(fù)責(zé)是否進(jìn)行表解析的判斷
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;
//進(jìn)入sql解析
mpBs.sql(this.changeTable(mpBs.sql()));
}根據(jù)INTERCEPTOR_IGNORE_CACHE中的緩存判斷是否進(jìn)入攔截方法
public static boolean willIgnore(String id, Function<InterceptorIgnoreCache, Boolean> function) {
//獲取sql方法對(duì)應(yīng)的注解緩存
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解析,解析出表名進(jìn)入業(yè)務(wù)方法。
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);
//進(jìn)入業(yè)務(wù)方法
builder.append(tableNameHandler.dynamicTableName(sql, name.getValue()));
}
last = name.getEnd();
}
if (last != sql.length()) {
builder.append(sql.substring(last));
}
return builder.toString();
}表解析,獲取表名給使用者在業(yè)務(wù)方法中進(jìn)行處理。
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字符串,認(rèn)為后續(xù)緊跟表名
if (hasMoreTokens(tokens, index)) {
SqlToken next = tokens.get(index++);
visitNameToken(next, visitor);
}
}
}
}
}四、總結(jié)
對(duì)于需要按照業(yè)務(wù)情況分表的情況有很對(duì),對(duì)應(yīng)的工具也有很多,本文主要是表過期就會(huì)進(jìn)行刪除,所以沒有必要使用sharding的分區(qū)方案,采用了Mybatis的動(dòng)態(tài)攔截。
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
java中List去除重復(fù)數(shù)據(jù)的5種方式總結(jié)
這篇文章主要給大家總結(jié)介紹了關(guān)于java中List去除重復(fù)數(shù)據(jù)的5種方式,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2021-01-01
SpringBoot如何使用Scala進(jìn)行開發(fā)的實(shí)現(xiàn)
這篇文章主要介紹了SpringBoot如何使用Scala進(jìn)行開發(fā)的實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-12-12
Java集合框架實(shí)戰(zhàn)應(yīng)用完全指南
本文總結(jié)Java集合框架中ArrayList、LinkedList、HashSet、LinkedHashSet和TreeSet的使用場景,結(jié)合實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友參考下吧2025-09-09
Spring Task定時(shí)任務(wù)每天零點(diǎn)執(zhí)行一次的操作
這篇文章主要介紹了Spring Task定時(shí)任務(wù)每天零點(diǎn)執(zhí)行一次的操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2020-09-09
Java對(duì)接阿里云短信服務(wù)保姆級(jí)教程(新手秒會(huì))
這篇文章主要介紹了如何在阿里云上申請(qǐng)短信服務(wù)以及如何使用Java代碼進(jìn)行對(duì)接,包括申請(qǐng)資質(zhì)、簽名和模板,以及編寫Java代碼整合成工具類進(jìn)行調(diào)用的步驟,需要的朋友可以參考下2024-12-12
macOS中搭建Java8開發(fā)環(huán)境(基于Intel?x86?64-bit)
這篇文章主要介紹了macOS中搭建Java8開發(fā)環(huán)境(基于Intel?x86?64-bit)?的相關(guān)資料,需要的朋友可以參考下2022-12-12

