用Mybatis手寫一個(gè)分表插件

這篇文章主要介紹“用Mybatis手寫一個(gè)分表插件”,在日常操作中,相信很多人在用Mybatis手寫一個(gè)分表插件問題上存在疑惑,小編查閱了各式資料,整理出簡(jiǎn)單好用的操作方法,希望對(duì)大家解答”用Mybatis手寫一個(gè)分表插件”的疑惑有所幫助!接下來,請(qǐng)跟著小編一起來學(xué)習(xí)吧!

創(chuàng)新互聯(lián)建站成都網(wǎng)站建設(shè)按需開發(fā)網(wǎng)站,是成都網(wǎng)站開發(fā)公司,為成都玻璃隔斷提供網(wǎng)站建設(shè)服務(wù),有成熟的網(wǎng)站定制合作流程,提供網(wǎng)站定制設(shè)計(jì)服務(wù):原型圖制作、網(wǎng)站創(chuàng)意設(shè)計(jì)、前端HTML5制作、后臺(tái)程序開發(fā)等。成都網(wǎng)站營(yíng)銷推廣熱線:13518219792

我們提供的服務(wù)有:網(wǎng)站建設(shè)、網(wǎng)站設(shè)計(jì)、微信公眾號(hào)開發(fā)、網(wǎng)站優(yōu)化、網(wǎng)站認(rèn)證、三門ssl等。為成百上千企事業(yè)單位解決了網(wǎng)站和推廣的問題。提供周到的售前咨詢和貼心的售后服務(wù),是有科學(xué)管理、有技術(shù)的三門網(wǎng)站制作公司

背景

事情是醬紫的,阿星的上級(jí)leader負(fù)責(zé)記錄信息的業(yè)務(wù),每日預(yù)估數(shù)據(jù)量是15萬左右,所以引入sharding-jdbc做分表。

上級(jí)leader完成業(yè)務(wù)的開發(fā)后,走了一波自測(cè),git push后,就忙其他的事情去了。

項(xiàng)目的框架是SpringBoot+Mybaits

出問題了

阿星負(fù)責(zé)的業(yè)務(wù)也開發(fā)完了,熟練的git pull,準(zhǔn)備自測(cè),單元測(cè)試run一下,上個(gè)廁所回來收工,就是這么自信。

回來后,看下控制臺(tái),人都傻了,一片紅,內(nèi)心不禁感嘆“如果這是股票基金該多好”。

出了問題就要解決,隨著排查深入,我的眉頭一皺發(fā)現(xiàn)事情并不簡(jiǎn)單,怎么以前的一些代碼都報(bào)錯(cuò)了?

隨著排查深入,最后跟到了Mybatis源碼,發(fā)現(xiàn)罪魁禍?zhǔn)资莝harding-jdbc引起的,因?yàn)閿?shù)據(jù)源是sharding-jdbc的,導(dǎo)致后續(xù)執(zhí)行sql的是ShardingPreparedStatement。

這就意味著,sharding-jdbc影響項(xiàng)目的所有業(yè)務(wù)表,因?yàn)樽罱K數(shù)據(jù)庫(kù)交互都由ShardingPreparedStatement去做了,歷史的一些sql語句因?yàn)閟ql函數(shù)或者其他寫法,使得ShardingPreparedStatement無法處理而出現(xiàn)異常。

關(guān)鍵代碼如下

用Mybatis手寫一個(gè)分表插件

發(fā)現(xiàn)問題后,阿星馬上就反饋給leader了。

用Mybatis手寫一個(gè)分表插件

唉,本來還想摸魚的,看來摸魚的時(shí)間是沒了,還多了一項(xiàng)任務(wù)。

分析

竟然交給阿星來做了,就擼起袖子開干吧,先看看分表功能的需求

  • 支持自定義分表策略

  • 能控制影響范圍

  • 通用性

分表會(huì)提前建立好,所以不需要考慮表不存在的問題,核心邏輯實(shí)現(xiàn),通過分表策略得到分表名,再把分表名動(dòng)態(tài)替換到sql。

用Mybatis手寫一個(gè)分表插件

分表策略

為了支持分表策略,我們需要先定義分表策略抽象接口,定義如下

/**  * @Author 程序猿阿星  * @Description 分表策略接口  * @Date 2021/5/9  */ public interface ITableShardStrategy {       /**      * @author: 程序猿阿星      * @description: 生成分表名      * @param tableNamePrefix 表前綴名      * @param value 值      * @date: 2021/5/9      * @return: java.lang.String      */     String generateTableName(String tableNamePrefix,Object value);      /**      * 驗(yàn)證tableNamePrefix      */     default void verificationTableNamePrefix(String tableNamePrefix){         if (StrUtil.isBlank(tableNamePrefix)) {             throw new RuntimeException("tableNamePrefix is null");         }     } }

generateTableName函數(shù)的任務(wù)就是生成分表名,入?yún)⒂衪ableNamePrefix、value,tableNamePrefix為分表前綴,value作為生成分表名的邏輯參數(shù)。

verificationTableNamePrefix函數(shù)驗(yàn)證tableNamePrefix必填,提供給實(shí)現(xiàn)類使用。

為了方便理解,下面是id取模策略代碼,取模兩張表

/**  * @Author 程序猿阿星  * @Description 分表策略id  * @Date 2021/5/9  */ @Component public class TableShardStrategyId implements ITableShardStrategy {     @Override     public String generateTableName(String tableNamePrefix, Object value) {         verificationTableNamePrefix(tableNamePrefix);         if (value == null || StrUtil.isBlank(value.toString())) {             throw new RuntimeException("value is null");         }         long id = Long.parseLong(value.toString());         //此處可以緩存優(yōu)化         return tableNamePrefix + "_" + (id % 2);     } }

傳入進(jìn)來的value是id值,用tableNamePrefix拼接id取模后的值,得到分表名返回。

控制影響范圍

分表策略已經(jīng)抽象出來,下面要考慮控制影響范圍,我們都知道Mybatis規(guī)范中每個(gè)Mapper類對(duì)應(yīng)一張業(yè)務(wù)主體表,Mapper類的函數(shù)對(duì)應(yīng)業(yè)務(wù)主體表的相關(guān)sql。

阿星想著,可以給Mapper類打上注解,代表該Mpaaer類對(duì)應(yīng)的業(yè)務(wù)主體表有分表需求,從規(guī)范來說Mapper類的每個(gè)函數(shù)對(duì)應(yīng)的主體表都是正確的,但是有些同學(xué)可能不會(huì)按規(guī)范來寫。

假設(shè)Mpaaer類對(duì)應(yīng)的是B表,Mpaaer類的某個(gè)函數(shù)寫著A表的sql,甚至是歷史遺留問題,所以注解不僅僅可以打在Mapper類上,同時(shí)還可以打在Mapper類的任意一個(gè)函數(shù)上,并且保證小粒度覆蓋粗粒度。

阿星這里自定義分表注解,代碼如下

/**  * @Author 程序猿阿星  * @Description 分表注解  * @Date 2021/5/9  */ @Target(value = {ElementType.TYPE,ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) public @interface TableShard {      // 表前綴名     String tableNamePrefix();      //值     String value() default "";      //是否是字段名,如果是需要解析請(qǐng)求參數(shù)改字段名的值(默認(rèn)否)     boolean fieldFlag() default false;      // 對(duì)應(yīng)的分表策略類     Class<? extends ITableShardStrategy> shardStrategy();   }

注解的作用范圍是類、接口、函數(shù),運(yùn)行時(shí)生效。

tableNamePrefix與shardStrategy屬性都好理解,表前綴名和分表策略,剩下的value與fieldFlag要怎么理解,分表策略分兩類,第一類依賴表中某個(gè)字段值,第二類則不依賴。

根據(jù)企業(yè)id取模,屬于第一類,此處的value設(shè)置企業(yè)id入?yún)⒆侄蚊?,fieldFlag為true,意味著,會(huì)去解析獲取企業(yè)id字段名對(duì)應(yīng)的值。

根據(jù)日期分表,屬于第二類,直接在分表策略實(shí)現(xiàn)類里面寫就行了,不依賴表字段值,value與fieldFlag無需填寫,當(dāng)然你value也可以設(shè)置時(shí)間格式,具體看分表策略實(shí)現(xiàn)類的邏輯。

通用性

抽象分表策略與分表注解都搞定了,最后一步就是根據(jù)分表注解信息,去執(zhí)行分表策略得到分表名,再把分表名動(dòng)態(tài)替換到sql中,同時(shí)具有通用性。

Mybatis框架中,有攔截器機(jī)制做擴(kuò)展,我們只需要攔截StatementHandler#prepare函數(shù),即StatementHandle創(chuàng)建Statement之前,先把sql里面的表名動(dòng)態(tài)替換成分表名。

Mybatis分表攔截器流程圖如下

用Mybatis手寫一個(gè)分表插件

Mybatis分表攔截器代碼如下,有點(diǎn)長(zhǎng)哈,主流程看intercept函數(shù)就好了。

/**  * @Author 程序員阿星  * @Description 分表攔截器  * @Date 2021/5/9  */ @Intercepts({         @Signature(                 type = StatementHandler.class,                 method = "prepare",                 args = {Connection.class, Integer.class}         ) }) public class TableShardInterceptor implements Interceptor {      private static final ReflectorFactory defaultReflectorFactory = new DefaultReflectorFactory();      @Override     public Object intercept(Invocation invocation) throws Throwable {          // MetaObject是mybatis里面提供的一個(gè)工具類,類似反射的效果         MetaObject metaObject = getMetaObject(invocation);         BoundSql boundSql = (BoundSql) metaObject.getValue("delegate.boundSql");         MappedStatement mappedStatement = (MappedStatement)                 metaObject.getValue("delegate.mappedStatement");          //獲取Mapper執(zhí)行方法         Method method = invocation.getMethod();          //獲取分表注解         TableShard tableShard = getTableShard(method,mappedStatement);          // 如果method與class都沒有TableShard注解或執(zhí)行方法不存在,執(zhí)行下一個(gè)插件邏輯         if (tableShard == null) {             return invocation.proceed();         }          //獲取值         String value = tableShard.value();         //value是否字段名,如果是,需要解析請(qǐng)求參數(shù)字段名的值         boolean fieldFlag = tableShard.fieldFlag();          if (fieldFlag) {             //獲取請(qǐng)求參數(shù)             Object parameterObject = boundSql.getParameterObject();              if (parameterObject instanceof MapperMethod.ParamMap) { //ParamMap類型邏輯處理                  MapperMethod.ParamMap parameterMap = (MapperMethod.ParamMap) parameterObject;                 //根據(jù)字段名獲取參數(shù)值                 Object valueObject = parameterMap.get(value);                 if (valueObject == null) {                     throw new RuntimeException(String.format("入?yún)⒆侄?s無匹配", value));                 }                 //替換sql                 replaceSql(tableShard, valueObject, metaObject, boundSql);              } else { //單參數(shù)邏輯                  //如果是基礎(chǔ)類型拋出異常                 if (isBaseType(parameterObject)) {                     throw new RuntimeException("單參數(shù)非法,請(qǐng)使用@Param注解");                 }                  if (parameterObject instanceof Map){                     Map<String,Object>  parameterMap =  (Map<String,Object>)parameterObject;                     Object valueObject = parameterMap.get(value);                     //替換sql                     replaceSql(tableShard, valueObject, metaObject, boundSql);                 } else {                     //非基礎(chǔ)類型對(duì)象                     Class<?> parameterObjectClass = parameterObject.getClass();                     Field declaredField = parameterObjectClass.getDeclaredField(value);                     declaredField.setAccessible(true);                     Object valueObject = declaredField.get(parameterObject);                     //替換sql                     replaceSql(tableShard, valueObject, metaObject, boundSql);                 }             }          } else {//無需處理parameterField             //替換sql             replaceSql(tableShard, value, metaObject, boundSql);         }         //執(zhí)行下一個(gè)插件邏輯         return invocation.proceed();     }       @Override     public Object plugin(Object target) {         // 當(dāng)目標(biāo)類是StatementHandler類型時(shí),才包裝目標(biāo)類,否者直接返回目標(biāo)本身, 減少目標(biāo)被代理的次數(shù)         if (target instanceof StatementHandler) {             return Plugin.wrap(target, this);         } else {             return target;         }     }       /**      * @param object      * @methodName: isBaseType      * @author: 程序員阿星      * @description: 基本數(shù)據(jù)類型驗(yàn)證,true是,false否      * @date: 2021/5/9      * @return: boolean      */     private boolean isBaseType(Object object) {         if (object.getClass().isPrimitive()                 || object instanceof String                 || object instanceof Integer                 || object instanceof Double                 || object instanceof Float                 || object instanceof Long                 || object instanceof Boolean                 || object instanceof Byte                 || object instanceof Short) {             return true;         } else {             return false;         }     }      /**      * @param tableShard 分表注解      * @param value      值      * @param metaObject mybatis反射對(duì)象      * @param boundSql   sql信息對(duì)象      * @author: 程序猿阿星      * @description: 替換sql      * @date: 2021/5/9      * @return: void      */     private void replaceSql(TableShard tableShard, Object value, MetaObject metaObject, BoundSql boundSql) {         String tableNamePrefix = tableShard.tableNamePrefix();         //獲取策略class         Class<? extends ITableShardStrategy> strategyClazz = tableShard.shardStrategy();         //從spring ioc容器獲取策略類          ITableShardStrategy tableShardStrategy = SpringUtil.getBean(strategyClazz);         //生成分表名         String shardTableName = tableShardStrategy.generateTableName(tableNamePrefix, value);         // 獲取sql         String sql = boundSql.getSql();         // 完成表名替換         metaObject.setValue("delegate.boundSql.sql", sql.replaceAll(tableNamePrefix, shardTableName));     }      /**      * @param invocation      * @author: 程序猿阿星      * @description: 獲取MetaObject對(duì)象-mybatis里面提供的一個(gè)工具類,類似反射的效果      * @date: 2021/5/9      * @return: org.apache.ibatis.reflection.MetaObject      */     private MetaObject getMetaObject(Invocation invocation) {         StatementHandler statementHandler = (StatementHandler) invocation.getTarget();         // MetaObject是mybatis里面提供的一個(gè)工具類,類似反射的效果         MetaObject metaObject = MetaObject.forObject(statementHandler,                 SystemMetaObject.DEFAULT_OBJECT_FACTORY,                 SystemMetaObject.DEFAULT_OBJECT_WRAPPER_FACTORY,                 defaultReflectorFactory         );          return metaObject;     }      /**      * @author: 程序猿阿星      * @description: 獲取分表注解      * @param method      * @param mappedStatement      * @date: 2021/5/9      * @return: com.xing.shard.interceptor.TableShard      */     private TableShard getTableShard(Method method, MappedStatement mappedStatement) throws ClassNotFoundException {         String id = mappedStatement.getId();         //獲取Class         final String className = id.substring(0, id.lastIndexOf("."));         //分表注解         TableShard tableShard = null;         //獲取Mapper執(zhí)行方法的TableShard注解         tableShard = method.getAnnotation(TableShard.class);         //如果方法沒有設(shè)置注解,從Mapper接口上面獲取TableShard注解         if (tableShard == null) {             // 獲取TableShard注解             tableShard = Class.forName(className).getAnnotation(TableShard.class);         }         return tableShard;     }  }

到了這里,其實(shí)分表功能就已經(jīng)完成了,我們只需要把分表策略抽象接口、分表注解、分表攔截器抽成一個(gè)通用jar包,需要使用的項(xiàng)目引入這個(gè)jar,然后注冊(cè)分表攔截器,自己根據(jù)業(yè)務(wù)需求實(shí)現(xiàn)分表策略,在給對(duì)應(yīng)的Mpaaer加上分表注解就好了。

用Mybatis手寫一個(gè)分表插件

實(shí)踐跑起來

這里阿星單獨(dú)寫了一套demo,場(chǎng)景是有兩個(gè)分表策略,表也提前建立好了

  • 根據(jù)id分表

    • tb_log_id_0

    • tb_log_id_1

  • 根據(jù)日期分表

    • tb_log_date_202105

    • tb_log_date_202106

預(yù)警:后面都是代碼實(shí)操環(huán)節(jié),請(qǐng)各位讀者大大耐心看完(非Java開發(fā)除外)。

TableShardStrategy定義

/**  * @Author wx  * @Description 分表策略日期  * @Date 2021/5/9  */ @Component public class TableShardStrategyDate implements ITableShardStrategy {      private static final String DATE_PATTERN = "yyyyMM";      @Override     public String generateTableName(String tableNamePrefix, Object value) {         verificationTableNamePrefix(tableNamePrefix);         if (value == null || StrUtil.isBlank(value.toString())) {             return tableNamePrefix + "_" +DateUtil.format(new Date(), DATE_PATTERN);         } else {             return tableNamePrefix + "_" +DateUtil.format(new Date(), value.toString());         }     } }    **  * @Author 程序猿阿星  * @Description 分表策略id  * @Date 2021/5/9  */ @Component public class TableShardStrategyId implements ITableShardStrategy {     @Override     public String generateTableName(String tableNamePrefix, Object value) {         verificationTableNamePrefix(tableNamePrefix);         if (value == null || StrUtil.isBlank(value.toString())) {             throw new RuntimeException("value is null");         }         long id = Long.parseLong(value.toString());         //可以加入本地緩存優(yōu)化         return tableNamePrefix + "_" + (id % 2);     } }

Mapper定義

Mapper接口

/**  * @Author 程序猿阿星  * @Description  * @Date 2021/5/8  */ @TableShard(tableNamePrefix = "tb_log_date",shardStrategy = TableShardStrategyDate.class) public interface LogDateMapper {      /**      * 查詢列表-根據(jù)日期分表      */     List<LogDate> queryList();      /**      * 單插入-根據(jù)日期分表      */     void  save(LogDate logDate);  }   -------------------------------------------------------------------------------------------------   /**  * @Author 程序猿阿星  * @Description  * @Date 2021/5/8  */ @TableShard(tableNamePrefix = "tb_log_id",value = "id",fieldFlag = true,shardStrategy = TableShardStrategyId.class) public interface LogIdMapper {      /**      * 根據(jù)id查詢-根據(jù)id分片      */     LogId queryOne(@Param("id") long id);      /**      * 單插入-根據(jù)id分片      */     void save(LogId logId);   }

Mapper.xml

<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper         PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"         "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.xing.shard.mapper.LogDateMapper">          //對(duì)應(yīng)LogDateMapper#queryList函數(shù)     <select id="queryList" resultType="com.xing.shard.entity.LogDate">         select         id as id,         comment as comment,         create_date as createDate         from         tb_log_date     </select>          //對(duì)應(yīng)LogDateMapper#save函數(shù)     <insert id="save" >         insert into tb_log_date(id, comment,create_date)         values (#{id}, #{comment},#{createDate})     </insert> </mapper>  -------------------------------------------------------------------------------------------------  <?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper         PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"         "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.xing.shard.mapper.LogIdMapper">          //對(duì)應(yīng)LogIdMapper#queryOne函數(shù)     <select id="queryOne" resultType="com.xing.shard.entity.LogId">         select         id as id,         comment as comment,         create_date as createDate         from         tb_log_id         where         id = #{id}     </select>          //對(duì)應(yīng)save函數(shù)     <insert id="save" >         insert into tb_log_id(id, comment,create_date)         values (#{id}, #{comment},#{createDate})     </insert>  </mapper>

執(zhí)行下單元測(cè)試

日期分表單元測(cè)試執(zhí)行

@Test   void test() {       LogDate logDate = new LogDate();       logDate.setId(snowflake.nextId());       logDate.setComment("測(cè)試內(nèi)容");       logDate.setCreateDate(new Date());       //插入       logDateMapper.save(logDate);       //查詢       List<LogDate> logDates = logDateMapper.queryList();       System.out.println(JSONUtil.toJsonPrettyStr(logDates));   }

輸出結(jié)果

用Mybatis手寫一個(gè)分表插件

id分表單元測(cè)試執(zhí)行

@Test void test() {     LogId logId = new LogId();     long id = snowflake.nextId();     logId.setId(id);     logId.setComment("測(cè)試");     logId.setCreateDate(new Date());     //插入     logIdMapper.save(logId);     //查詢     LogId logIdObject = logIdMapper.queryOne(id);     System.out.println(JSONUtil.toJsonPrettyStr(logIdObject)); }

輸出結(jié)果

用Mybatis手寫一個(gè)分表插件

小結(jié)一下

本文可以當(dāng)做對(duì)Mybatis進(jìn)階的使用教程,通過Mybatis攔截器實(shí)現(xiàn)分表的功能,滿足基本的業(yè)務(wù)需求,雖然比較簡(jiǎn)陋,但是Mybatis這種擴(kuò)展機(jī)制與設(shè)計(jì)值得學(xué)習(xí)思考。

到此,關(guān)于“用Mybatis手寫一個(gè)分表插件”的學(xué)習(xí)就結(jié)束了,希望能夠解決大家的疑惑。理論與實(shí)踐的搭配能更好的幫助大家學(xué)習(xí),快去試試吧!若想繼續(xù)學(xué)習(xí)更多相關(guān)知識(shí),請(qǐng)繼續(xù)關(guān)注創(chuàng)新互聯(lián)網(wǎng)站,小編會(huì)繼續(xù)努力為大家?guī)砀鄬?shí)用的文章!

本文標(biāo)題:用Mybatis手寫一個(gè)分表插件
網(wǎng)頁(yè)網(wǎng)址:http://bm7419.com/article46/igcphg.html

成都網(wǎng)站建設(shè)公司_創(chuàng)新互聯(lián),為您提供營(yíng)銷型網(wǎng)站建設(shè)、網(wǎng)站策劃響應(yīng)式網(wǎng)站網(wǎng)站維護(hù)、網(wǎng)站改版企業(yè)建站

廣告

聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶投稿、用戶轉(zhuǎn)載內(nèi)容為主,如果涉及侵權(quán)請(qǐng)盡快告知,我們將會(huì)在第一時(shí)間刪除。文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如需處理請(qǐng)聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時(shí)需注明來源: 創(chuàng)新互聯(lián)

外貿(mào)網(wǎng)站制作