Mybatis之通用Mapper動態(tài)表名及其原理分析
一、引言
單表增刪改查的重復(fù)書寫相當(dāng)冗余,目前為了避免這樣的冗余我們會使用通用mapper,但是當(dāng)遇到表名動態(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都是對當(dāng)天的表進行操作,但是總有操作不是針對當(dāng)天的,例如創(chuàng)建、刪除表、查詢過往數(shù)據(jù)。
初期走了一些彎路,本來是想使用@MapperScan({"***.domain.mapper"})限制這個攔截配置類的作用范圍,將攔截限制在固定路徑下,然后將不需要攔截的單獨在其他路徑下編寫。
但是這個攔截器是注冊在Mybatis內(nèi)部,底層還是使用Mybatis的攔截sql機制,所以限制作用范圍是不起作用的,具體內(nèi)容感興趣的可以看原理分析。
回歸正題,那么如果不攔截該sql呢?通過查閱通用Mapper的相關(guān)文檔了解到有一個注解可以使用。
對于通用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ù)權(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"+"@"+可選項[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對于sql方法的攔截,動態(tài)表名等攔截器實際上只是注冊到了它的局部變量interceptors中,所以在Mybatis統(tǒng)一的攔截機制下,給注冊的攔截器設(shè)置作用范圍也就不會生效了。
@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負責(zé)是否進行表解析的判斷
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方法對應(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解析,解析出表名進入業(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); //進入業(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ù)方法中進行處理。
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é)
對于需要按照業(yè)務(wù)情況分表的情況有很對,對應(yīng)的工具也有很多,本文主要是表過期就會進行刪除,所以沒有必要使用sharding的分區(qū)方案,采用了Mybatis的動態(tài)攔截。
以上為個人經(jīng)驗,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關(guān)文章
淺談virtual、abstract方法和靜態(tài)方法、靜態(tài)變量理解
下面小編就為大家?guī)硪黄獪\談virtual、abstract方法和靜態(tài)方法、靜態(tài)變量理解。小編覺得挺不錯的,現(xiàn)在就分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2017-02-02Spring Boot Web 靜態(tài)文件緩存處理的方法
本篇文章主要介紹了Spring Boot Web 靜態(tài)文件緩存處理的方法,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2018-02-02RocketMQ?NameServer架構(gòu)設(shè)計啟動流程
這篇文章主要為大家介紹了RocketMQ?NameServer架構(gòu)設(shè)計啟動流程,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-02-02