Spring Boot Mybatis實現(xiàn)分表

? ? ??開發(fā)過程中可能會碰到分表的場景章办,數(shù)據(jù)庫的數(shù)據(jù)量相當(dāng)大的時候可能需要按天分表或者按月分表啥的(分表策略)罢洲。接下來就教大家用最簡單的方式實現(xiàn)這一需求食寡。

? ? ??咱們接下來主要實現(xiàn)以下兩個大功能:

  • 自動建表丸边,當(dāng)表不存在的時候自動把表創(chuàng)建出來。
  • 自動分表崎逃,根據(jù)操作數(shù)據(jù)庫的某個參數(shù)進行分表。

? ? ??自動建表眉孩,自動分表核心思想在Mybatis攔截器的使用个绍。強烈建議大家先去了解下Mybatis攔截器的使用(之前也寫過一遍關(guān)于Mybatis攔截器的使用的文章,有興趣的可以看下 http://www.reibang.com/p/8d8638d03189 )勺像。

? ? ??根據(jù)實際情況我們做如下規(guī)定:

  • 每個需要分表的表都有一個基礎(chǔ)表名障贸。比如按月分表之后的表名為“XXX-201909”,那么我們認(rèn)為"XXX"就是基礎(chǔ)表名吟宦。所有的sql語句里面還是用基礎(chǔ)表名篮洁,我們會在自定義Mybatis攔截器里面找到基礎(chǔ)表名替換成分表表名。
  • 分表的依據(jù)來源于操作數(shù)據(jù)庫的參數(shù)當(dāng)中的一個殃姓。我們會通過參數(shù)注解(TableShardParam)來標(biāo)識哪個操作作為分表依據(jù)袁波。
  • 每個分表需要自己指定分表策略(ITableNameStrategy),針對每個分表我們需要自己去實現(xiàn)自己的分表策略蜗侈,自己實現(xiàn)ITableNameStrategy接口篷牌。

一 自動建表準(zhǔn)備

? ? ??我們考慮到大部分分表的情況下,都希望在代碼里面能夠自動建表踏幻。操作表之前判斷表是否存在枷颊,如果表不存在則自動幫我們把表建出來。

? ? ??關(guān)于自動建表该面,結(jié)合實際情況夭苗,我們認(rèn)為建表是和每個表對應(yīng)的實體類綁定在一起的。所以我們會有一個建表相關(guān)的TableCreate注解隔缀,TableCreate注解是添加在每個表對應(yīng)的實體類上的题造。TableCreate注解的元數(shù)據(jù)會告訴我們當(dāng)前實體類對應(yīng)表的基礎(chǔ)表名,已經(jīng)去哪里找到相關(guān)的建表語句猾瘸。

TableCreate注解需要添加在表對應(yīng)的實體類上

/**
 * @name: TableCreate
 * @author: tuacy.
 * @date: 2019/8/29.
 * @version: 1.0
 * @Description: TableCreate注解用于告訴我們怎么找到建表語句(如果表不存在的情況下, 我們程序里面自己去建表)
 * <p>
 * tableName -- 基礎(chǔ)表名
 * autoCreateTableMapperClass -- mapper class對應(yīng)的名字
 * autoCreateTableMapperMethodName -- mapper class 里面對應(yīng)的方法
 * <p>
 * 最終我們會去mapper class里面找到對應(yīng)的對應(yīng)的方法,最終拿到建表語句
 */
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface TableCreate {

    /**
     * table的基礎(chǔ)表名
     */
    String tableName();

    /**
     * Mapper類界赔,不能為空
     */
    Class<?> autoCreateTableMapperClass();

    /**
     * Mapper文件里面的函數(shù)名字(創(chuàng)建表對應(yīng)的函數(shù))
     */
    String autoCreateTableMapperMethodName();
}

? ? ??為了方便Mybatis攔截器里面自動建表的使用丢习,每個表對應(yīng)的建表信息我們用TableCreateConfig類做一個簡單的分裝。TableCreateConfig會告訴我們基礎(chǔ)表名淮悼,以及我們需要的建表語句在哪個Mapper類哪個方法里面咐低。


/**
 * @name: TableCreateConfig
 * @author: tuacy.
 * @date: 2019/8/29.
 * @version: 1.0
 * @Description: 自動建表相關(guān)的一些配置信息
 * 在攔截器里面我們會根據(jù)autoCreateTableMapperClass類的autoCreateTableMapperMethodName方法找到建表語句
 */
@Data
@Accessors(chain = true)
public class TableCreateConfig {

    /**
     * 表名
     */
    private String tableName;

    /**
     * 自動建表Mapper類
     */
    private Class<?> autoCreateTableMapperClass;

    /**
     * 自動建表Mapper中的方法
     */
    private String autoCreateTableMapperMethodName;

}

? ? ??在Spring Boot啟動的時候,我們會表實體類對應(yīng)的包下面讀取所有添加了TableCreate注解的相關(guān)信息敛惊,把讀取的信息封裝到TableCreateConfig類里面渊鞋,并且保存在單例類TableCreateManager里面。這一部分內(nèi)容大家可以看下我給出的源碼里面TableCreateScan瞧挤,TableCreateScanRegister類里面邏輯锡宋。

? ? ??簡單總結(jié)下關(guān)于自動建表我們做了那些準(zhǔn)備工作。我們會在Spring Boot啟動的過程中去讀取所有添加了TableCreate注解的實體類特恬。把讀取到的信息保存在單例類TableCreateManager里面执俩。單例TableCreateManager里面會維護一個Map:key就是每個需要建表的基礎(chǔ)表名,value則是建表相關(guān)的信息癌刽。建表相關(guān)的信息會和Mapper里面的某個方法關(guān)聯(lián)起來役首。具體可以看下下面Mybatis攔截器的具體實現(xiàn)。

二 自動分表準(zhǔn)備

? ? ??分表显拜,我們需要兩個東西:分表策略衡奥、分表依據(jù)。

2.1 分表策略

? ? ??分表策略远荠,我們定義一個分表接口矮固,讓每個分表去實現(xiàn)直接的分表策略。分表策略我們給兩個參數(shù)譬淳,一個是基礎(chǔ)表名档址,一個是分表依據(jù)。

/**
 * @name: ITableNameStrategy
 * @author: tuacy.
 * @date: 2019/8/13.
 * @version: 1.0
 * @Description: 分表對應(yīng)的策略
 */
public interface ITableNameStrategy {

    /**
     * 表名字
     *
     * @param oldTableName     表基本名字
     * @param dependFieldValue 根據(jù)該字段確定表名(Mapper方法的某個參數(shù)對應(yīng)的值)
     * @return 表名
     */
    String tableName(String oldTableName, String dependFieldValue);

}

? ? ??分表策略的配置邻梆,我們把他們放在操作數(shù)據(jù)庫的方法上守伸。在TablePrepare注解里面指定。TablePrepare注解也用于標(biāo)識是否進入我們自定義的Mybatis攔截器里面去浦妄。

/**
 * @name: TablePrepare
 * @author: tuacy.
 * @date: 2019/8/29.
 * @version: 1.0
 * @Description:
 */
@Documented
@Inherited
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface TablePrepare {

    /**
     * 啟用自動建表,當(dāng)表不存在的時候,是否創(chuàng)建表
     */
    boolean enableAutoCreateTable() default true;

    /**
     * 啟用分表
     */
    boolean enableTableShard() default false;

    /**
     * 指定表尼摹,如果設(shè)置該值,則只會處理指定的表剂娄,沒有則會處理sql中的所有表
     * 如果自己設(shè)置了基礎(chǔ)表的名字,那么我們處理建表和分表的時候只會處理這些指定的表.
     * 如果沒有設(shè)置基礎(chǔ)表的時候,我們會自動去sql語句里面解析出所有的表名.做相應(yīng)的建表和分表的邏輯
     */
    String[] appointTable() default {};

    /**
     * 表名策略窘问,通過某種規(guī)則得到表名
     */
    Class<? extends ITableNameStrategy> strategy() default TableNameStrategyVoid.class;
}

2.2 分表依據(jù)

? ? ??結(jié)合實際情況,我們認(rèn)為分表的依據(jù)都是來源于操作數(shù)據(jù)的某個參數(shù)(也可能是某個參數(shù)的某個字段)宜咒。那這里就有問題了,操作數(shù)據(jù)庫有的時候有多個參數(shù)把鉴,哪個參數(shù)作為分表依據(jù)呢故黑。我們定義一個參數(shù)注解TableShardParam儿咱。哪個參數(shù)添加了該注解,我們就認(rèn)為這個參數(shù)是分表依據(jù)(目前只支持一個參數(shù)作為依據(jù))场晶。我們會在我們自定義的Mybatis攔截器里面找到添加了TableShardParam注解的參數(shù)對應(yīng)的值混埠。

? ? ??為了應(yīng)對多種情況。TableShardParam支持以下幾種情況(這部分具體的實現(xiàn)诗轻,需要仔細(xì)看下下面自定義Mybatis攔截器里面這部分的具體實現(xiàn))钳宪。大家可以根據(jù)自己的實際情況做相應(yīng)的修改。

  • TableShardParam添加在java基礎(chǔ)類型上扳炬,比如int,long等,我們會把基礎(chǔ)類型轉(zhuǎn)換為String,最終傳遞給分表策略吏颖。
  • TableShardParam添加在對象類型上,我們可以找到對象的某個屬性(反射)對應(yīng)的值恨樟,最終傳遞給分表策略半醉。
  • TableShardParam添加在List上,我們會找到List對象的一個元素劝术,如果List里面的元素是java基礎(chǔ)類型缩多,直接獲取到第一個元素對應(yīng)的值,如果List里面的元素是對象养晋,則獲取到對象某個屬性對應(yīng)的值衬吆。在最終把他們傳遞給分表策略。
/**
 * @name: TableShardParam
 * @author: tuacy.
 * @date: 2019/8/30.
 * @version: 1.0
 * @Description: 添加在參數(shù)上的注解, 一定要配置mybatis 的Param注解使用
 * <p>
 * 我們是這樣考慮的,分表核心在于確定表的名字,表的名字怎么來,肯定是通過某個參數(shù)來獲取到.
 * 所以,這里我們設(shè)計TableShardParam注解,用于添加在參數(shù)上,讓我們方便的獲取到通過那個參數(shù)來獲取表名
 * 1. int insertItem(@TableShardParam(dependFieldName = "recTime") @Param("item") AccHour item);
 * -- 分表依據(jù)對應(yīng)AccHour對象recTime屬性對應(yīng)的值
 * 2. int insertList(@TableShardParam(dependFieldName = "recTime") @Param("list") List<AccHour> list);
 * -- 分表依據(jù)對應(yīng)list的第一個對象recTime屬性對應(yīng)的值
 * 3. List<AccHour> selectLIst(@TableShardParam() @Param("startTime") Long startTIme, @Param("endTime") Long endTime);
 * -- 分表依據(jù)對應(yīng)endTime對應(yīng)的值
 */
@Documented
@Inherited
@Target({ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface TableShardParam {

    @AliasFor("dependFieldName")
    String value() default "";

    /**
     * dependFieldName取到我們需要的獲取表名的依據(jù)
     */
    @AliasFor("value")
    String dependFieldName() default "";

}

三 自定義Mybatis攔截器

? ? ?? 關(guān)于自定義Mybatis攔截器的具體實現(xiàn)绳泉,我這里就直接貼代碼了逊抡。肯定有些地方是還沒有考慮到的圈纺,大家需要根據(jù)自己的需求做修改秦忿,重點關(guān)注以下幾個部分:

  • 攔截器里面我們是怎么拿到相應(yīng)的建表語句的。
  • 攔截器里面我們是怎么去執(zhí)行建表語句的蛾娶。
  • 攔截器里面我們是怎么拿到分表依據(jù)的灯谣,里面考慮了多種情況。
  • 每個sql語句蛔琅,我們是怎么解析出表名的胎许。怎么把我們把我們分表表名替換進去的。
/**
 * @name: TableShardInterceptor
 * @author: tuacy.
 * @date: 2019/8/13.
 * @version: 1.0
 * @Description: 自動建表 + 分表 攔截器的實現(xiàn)
 */
@Intercepts({
        @Signature(
                type = StatementHandler.class,
                method = "prepare",
                args = {Connection.class, Integer.class}
        )
})
public class TableShardInterceptor implements Interceptor {

    /**
     * sql語句里面去獲取表名的依據(jù)(主要罗售,全部是小寫的)
     * 說白了就是哪些字符串后面會跟上表名
     */
    private final static String[] SQL_TABLE_NAME_FLAG_PREFIX = {"from", "join", "update", "insert into"};

    private static final ObjectFactory DEFAULT_OBJECT_FACTORY = new DefaultObjectFactory();
    private static final ObjectWrapperFactory DEFAULT_OBJECT_WRAPPER_FACTORY = new DefaultObjectWrapperFactory();
    private static final ReflectorFactory REFLECTOR_FACTORY = new DefaultReflectorFactory();

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        if (!(invocation.getTarget() instanceof RoutingStatementHandler)) {
            return invocation.proceed();
        }

        try {
            RoutingStatementHandler statementHandler = (RoutingStatementHandler) invocation.getTarget();
            // MetaObject是mybatis里面提供的一個工具類辜窑,類似反射的效果
            MetaObject metaStatementHandler = MetaObject.forObject(statementHandler, DEFAULT_OBJECT_FACTORY, DEFAULT_OBJECT_WRAPPER_FACTORY, REFLECTOR_FACTORY);
            BoundSql boundSql = (BoundSql) metaStatementHandler.getValue("delegate.boundSql");//獲取sql語句
            String originSql = boundSql.getSql();

            if (StringUtils.isEmpty(originSql)) {
                return invocation.proceed();
            }

            MappedStatement mappedStatement = (MappedStatement) metaStatementHandler.getValue("delegate.mappedStatement");
            // 判斷方法上是否添加了 TableShardAnnotation 注解,因為只有添加了TableShard注解的方法我們才會去做分表處理
            TablePrepare tablePrepare = getTableShardAnnotation(mappedStatement);

            // 沒有加@TablePrepare注解則不填家我們自定義的邏輯
            if (tablePrepare == null) {
                return invocation.proceed();
            }

            boolean enableAutoCreateTable = tablePrepare.enableAutoCreateTable(); // 表不存在的是哈,事發(fā)創(chuàng)建
            boolean enableTableShard = tablePrepare.enableTableShard(); // 事發(fā)進行分表邏輯處理
            // 自動建表和分表是否開啟寨躁,都沒有則退出往下走
            if (!enableAutoCreateTable && !enableTableShard) {
                invocation.proceed();
            }

            // 獲取到需要處理的表名
            String[] appointTable = tablePrepare.appointTable();
            if (appointTable.length == 0) {
                List<String> tableNameList = getTableNamesFromSql(originSql);
                if (tableNameList == null || tableNameList.isEmpty()) {
                    return invocation.proceed();
                } else {
                    // 去掉前后空格和/n
                    tableNameList = tableNameList.stream().map(item -> {
                        if (item == null) {
                            return null;
                        }
                        return item.trim().replaceAll("[\r\n]", "");
                    }).collect(Collectors.toList());
                    appointTable = new String[tableNameList.size()];
                    tableNameList.toArray(appointTable);
                }
            }


            // 獲取分表表名處理策略
            Class<? extends ITableNameStrategy> strategyClass = tablePrepare.strategy();
            ITableNameStrategy tableStrategy = null;
            if (!strategyClass.equals(TableNameStrategyVoid.class)) {
                tableStrategy = strategyClass.newInstance();
            }

            // 分表處理的時候,我們一般是依賴參數(shù)里面的某個值來進行的.這里用于獲取到參數(shù)對應(yīng)的值.
            String dependValue = getDependFieldValue(tablePrepare, metaStatementHandler, mappedStatement);

            // 自動建表處理邏輯(表不存在的時候,我們會建表)
            if (tablePrepare.enableAutoCreateTable()) {
                SqlSessionTemplate template = SpringContextHolder.getBean(SqlSessionTemplate.class);
                for (String tableName : appointTable) {
                    TableCreateConfig classConfig = TableCreateManager.INSTANCE.getClassConfig(tableName);
                    if (classConfig == null) {
                        // 沒有找到建表語句則跳過
                        continue;
                    }

                    String createSqlMethodPath = classConfig.getAutoCreateTableMapperClass().getName() + "." + classConfig.getAutoCreateTableMapperMethodName();
                    String sql = template.getConfiguration().getMappedStatement(createSqlMethodPath).getBoundSql("delegate.boundSql").getSql();
                    if (StringUtils.isEmpty(sql)) {
                        // 建表sql為空時不理穆碎,直接跳過
                        continue;
                    }

                    if (!StringUtils.isEmpty(dependValue) && strategyClass != TableNameStrategyVoid.class) {
                        sql = sql.replace(tableName, tableStrategy.tableName(tableName, dependValue));
                    }

                    Connection conn = (Connection) invocation.getArgs()[0];
                    boolean preAutoCommitState = conn.getAutoCommit();
                    conn.setAutoCommit(false);//將自動提交關(guān)閉
                    try (PreparedStatement countStmt = conn.prepareStatement(sql)) {
                        // 把新語句設(shè)置回去
                        metaStatementHandler.setValue("delegate.boundSql.sql", sql);
                        countStmt.execute();
                        conn.commit();//執(zhí)行完后,手動提交事務(wù)
//                        System.out.println(isSuccess);
                    } catch (Exception e) {
                        e.printStackTrace();
                    } finally {
                        conn.setAutoCommit(preAutoCommitState);//在把自動提交打開
                    }
                }
            }

            // 分表處理邏輯
            if (strategyClass != TableNameStrategyVoid.class) {
                if (tablePrepare.enableTableShard()) {
                    String updateSql = originSql;
                    for (String tableName : appointTable) {
                        // 策略處理表名
                        String newTableName = tableStrategy.tableName(tableName, dependValue);
                        updateSql = updateSql.replaceAll(tableName, newTableName);
                    }

                    // 把新語句設(shè)置回去职恳,替換表名
                    metaStatementHandler.setValue("delegate.boundSql.sql", updateSql);
                }
            } else {
                // fix 啟用了自動建表所禀,但是沒有啟用分表的時候方面,sql被替換成建表的sql。沒有設(shè)置回來的問題
                metaStatementHandler.setValue("delegate.boundSql.sql", originSql);
            }
        } catch (Exception ignored) {
            // ignore 任何一個地方有異常都去執(zhí)行原始操作 -- invocation.proceed()
        }
        return invocation.proceed();
    }

    /**
     * 從參數(shù)里面找到指定對象指定字段對應(yīng)的值
     */
    private String getDependFieldValue(TablePrepare tablePrepare, MetaObject metaStatementHandler, MappedStatement mappedStatement) throws Exception {

        // 以上情況下不滿足則走@TableShardParam機制
        String id = mappedStatement.getId();
        String className = id.substring(0, id.lastIndexOf("."));
        String methodName = id.substring(id.lastIndexOf(".") + 1);
        Method[] methods = Class.forName(className).getMethods();
        Method method = null;
        for (Method me : methods) {
            if (me.getName().equals(methodName) && me.isAnnotationPresent(tablePrepare.annotationType())) {
                method = me;
            }
        }

        if (method == null) {
            return null;
        }

        Parameter[] parameters = method.getParameters();
        if (parameters.length == 0) {
            return null;
        }

        int flag = 0;
        Parameter parameter = null;
        for (Parameter p : parameters) {
            // TableShardParam和Param需要同時添加
            if (p.getAnnotation(TableShardParam.class) != null && p.getAnnotation(Param.class) != null) {
                parameter = p;
                flag++;

            }
        }

        // 參數(shù)沒有注解則退出
        if (flag == 0) {
            return null;
        }

        // 多個則拋異常
        if (flag > 1) {
            throw new RuntimeException("存在多個指定@TableShardParam的參數(shù)色徘,無法處理");
        }

        String tableSharedFieldParamKey = parameter.getAnnotation(Param.class).value();
        TableShardParam annotation = parameter.getAnnotation(TableShardParam.class);
        Class<?> parameterType = parameter.getType(); // 參數(shù)的類型
        String dependFieldName = StringUtils.isEmpty(annotation.value()) ? annotation.dependFieldName() : annotation.value();
        if (isPrimitive(parameterType) || StringUtils.isEmpty(dependFieldName)) {
            return getPrimitiveParamFieldValue(metaStatementHandler, tableSharedFieldParamKey);
        } else {
            return getParamObjectFiledValue(metaStatementHandler, tableSharedFieldParamKey, dependFieldName);
        }
    }

    /**
     * 判斷是否是基礎(chǔ)類型 9大基礎(chǔ)類型及其包裝類
     *
     * @return 是否是基礎(chǔ)類型, long, int, Long 等等
     */
    private boolean isPrimitive(Class<?> clazz) {
        if (clazz.isPrimitive()) {
            return true;
        }

        try {
            if (((Class) clazz.getField("TYPE").get(null)).isPrimitive()) {
                return true;
            }
        } catch (Exception e) {
            return false;
        }

        return clazz.equals(String.class);
    }

    /**
     * 解析sql獲取到sql里面所有的表名
     *
     * @param sql sql
     * @return 表名列表
     */
    private List<String> getTableNamesFromSql(String sql) {
        // 對sql語句進行拆分 -- 以'恭金,'、'\n'褂策、'\t'作為分隔符
        List<String> splitterList = Lists.newArrayList(Splitter.on(new CharMatcher() {
            @Override
            public boolean matches(char c) {
                return Character.isWhitespace(c) || c == '\n' || c == '\t';
            }
        }).omitEmptyStrings().trimResults().split(sql))
                .stream()
                .filter(s -> !s.equals(","))
                .filter(s -> !s.equals("?"))
                .filter(s -> !s.equals("?,"))
                .filter(s -> !s.equals("("))
                .filter(s -> !s.equals(")"))
                .filter(s -> !s.equals("="))
                .collect(Collectors.toList());
        List<String> tableNameList = Lists.newArrayList();
        for (String item : SQL_TABLE_NAME_FLAG_PREFIX) {
            tableNameList.addAll(getTableName(splitterList, Lists.newArrayList(Splitter.on(' ').split(item))));
        }
        return tableNameList;
    }

    /**
     * 獲取表名
     */
    private List<String> getTableName(List<String> splitterList, List<String> list) {
        List<String> retList = Lists.newArrayList();
        if (list == null || list.isEmpty() || splitterList == null || splitterList.isEmpty() || splitterList.size() <= list.size()) {
            return retList;
        }
        for (int index = 0; index < splitterList.size(); index = index + list.size()) {

            if (index < splitterList.size() - list.size()) {
                boolean match = true;
                for (int innerIndex = 0; innerIndex < list.size(); innerIndex++) {
                    if (!splitterList.get(index + innerIndex).toLowerCase().equals(list.get(innerIndex).toLowerCase())) {
                        match = false;
                        break;
                    }
                }
                if (match) {
                    if ("update".toLowerCase().equals(list.get(0).toLowerCase())) {
                        // ON DUPLICATE KEY UPDATE 需要過濾出來
                        if (index < 3 || !(splitterList.get(index - 1).toLowerCase().equals("key".toLowerCase()) &&
                                splitterList.get(index - 2).toLowerCase().equals("DUPLICATE".toLowerCase()) &&
                                splitterList.get(index - 3).toLowerCase().equals("ON".toLowerCase()))) {
                            retList.add(splitterList.get(index + list.size()));
                        }
                    } else {
                        retList.add(splitterList.get(index + list.size()));
                    }
                }
            }

        }
        return retList;
    }


    /**
     * 獲取方法上的TableShard注解
     *
     * @param mappedStatement MappedStatement
     * @return TableShard注解
     */
    private TablePrepare getTableShardAnnotation(MappedStatement mappedStatement) {
        TablePrepare tablePrepare = null;
        try {
            String id = mappedStatement.getId();
            String className = id.substring(0, id.lastIndexOf("."));
            String methodName = id.substring(id.lastIndexOf(".") + 1);
            final Method[] method = Class.forName(className).getMethods();
            for (Method me : method) {
                if (me.getName().equals(methodName) && me.isAnnotationPresent(TablePrepare.class)) {
                    tablePrepare = me.getAnnotation(TablePrepare.class);
                    break;
                }
            }
        } catch (Exception ex) {
            ex.printStackTrace();
        }
        return tablePrepare;
    }

    /**
     * 從參數(shù)里面找到指定對象指定字段對應(yīng)的值--基礎(chǔ)類型
     */
    private String getPrimitiveParamFieldValue(MetaObject metaStatementHandler, String fieldParamKey) {
        BoundSql boundSql = (BoundSql) metaStatementHandler.getValue("delegate.boundSql");
        Object parameterObject = boundSql.getParameterObject();
        if (parameterObject == null) {
            return null;
        }
        Object filterFiledObject = ((MapperMethod.ParamMap) parameterObject).get(fieldParamKey);
        if (filterFiledObject == null) {
            return null;
        }
        Object dependObject = recursiveGetEffectiveObject(filterFiledObject);
        return dependObject == null ? null : dependObject.toString();
    }

    /**
     * 獲取參數(shù)里面的對象
     */
    private Object recursiveGetEffectiveObject(Object srcObject) {

        if (!(srcObject instanceof List)) {
            return srcObject;
        }
        Object listItemObject = ((List) srcObject).get(0);
        while (listItemObject instanceof List) {
            listItemObject = ((List) listItemObject).get(0);
        }
        return listItemObject;
    }


    /**
     * 從參數(shù)里面找到指定對象指定字段對應(yīng)的值--對象
     * 如該參數(shù)是List.指定對象為第一個元素
     */
    private String getParamObjectFiledValue(MetaObject metaStatementHandler, String fieldParamKey, String dependFieldName) {

        BoundSql boundSql = (BoundSql) metaStatementHandler.getValue("delegate.boundSql");
        Object parameterObject = boundSql.getParameterObject();
        Object filterFiledObject = ((MapperMethod.ParamMap) parameterObject).get(fieldParamKey);

        if (filterFiledObject == null) {
            return null;
        }
        Object dependObject = recursiveGetEffectiveObject(filterFiledObject);
        try {
            return ReflectUtil.getFieldValue(dependObject, dependFieldName);
        } catch (Exception ignored) {
        }

        return null;
    }

    @Override
    public Object plugin(Object target) {
        // 當(dāng)目標(biāo)類是StatementHandler類型時横腿,才包裝目標(biāo)類,否者直接返回目標(biāo)本身,減少目標(biāo)被代理的次數(shù)
        return (target instanceof RoutingStatementHandler) ? Plugin.wrap(target, this) : target;
    }

    @Override
    public void setProperties(Properties properties) {
    }
}

四 怎么使用

? ? ??我們用一個簡單實例來教大家怎么使用我們實現(xiàn)的分表功能斤寂」⒑福基礎(chǔ)表名StatisAccHour,

4.1 建表語句

? ? ??和我們平常使用Mybatis一樣的扬蕊,一個Mapper接口和一個Mapper xml搀别。

public interface CreateTableMapper {

    int createAccHour();
}
<?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.tuacy.tableshard.mapper.CreateTableMapper">

    <!-- acc 小時表, 一個小時一張表 -->
    <update id="createAccHour">
        CREATE TABLE IF NOT EXISTS `StatisAccHour` (
            `recTime` bigint(20) NOT NULL,
            `ptId` int(11) NOT NULL,
            `value` double DEFAULT NULL,
            PRIMARY KEY (`RecTime`,`PtId`)
        ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
    </update>

</mapper>

4.2 表對應(yīng)實體類

? ? ??"StatisAccHour"基礎(chǔ)表對應(yīng)的實體類,三個字段和表里面的字段一一對應(yīng)尾抑。同時我們添加了TableCreate注解在實體了上歇父,指定了基礎(chǔ)表名“StatisAccHour”,建表語句在CreateTableMapper類的createAccHour方法里面再愈。

@TableCreate(
        tableName = "StatisAccHour",
        autoCreateTableMapperClass = CreateTableMapper.class,
        autoCreateTableMapperMethodName = "createAccHour"
)
@Getter // lombok 注解,不用手動去寫get set方法
@Setter
public class AccHour {

    /**
     * 針對recTime做一個簡單說明,
     * 比如當(dāng)前時間是 2019年08月31日00時31分46秒141微妙
     * 則我們在數(shù)據(jù)庫里面存20190831003146141
     */
    private Long recTime;
    private Long ptId;
    private Double value;

}

4.3 分表策略

? ? ??基礎(chǔ)表名和分表依據(jù)字段的前十個字符組成分表對應(yīng)的表名榜苫。


/**
 * 分表方案 按照年月日時分表
 */
public class SuffixYYYYMMDDHHNameStrategy implements ITableNameStrategy {

    private static final int SUFFIX_LENGTH = 10; // yyyymmddhh

    @Override
    public String tableName(String baseTableName, String dependFieldValue) {
        return baseTableName + dependFieldValue.substring(0, SUFFIX_LENGTH);
    }
}

4.4 數(shù)據(jù)庫操作

? ? ??注意TablePrepare注解的添加,每個sql里面的表名還是用的基礎(chǔ)表名翎冲。最終會在自定義攔截器里面替換垂睬。

/**
 * AccHour 每個小時一張表(多數(shù)據(jù)源,我們有三個數(shù)據(jù)源,我們假設(shè)該表放在statis數(shù)據(jù)源下面)
 */
public interface AccHourMapper {

    /**
     * 往數(shù)據(jù)庫里面插入一條記錄
     */
    @TablePrepare(enableTableShard = true, strategy = SuffixYYYYMMDDHHNameStrategy.class)
    int insertItem(@TableShardParam(dependFieldName = "recTime") @Param("item") AccHour item);

    /**
     * 往數(shù)據(jù)庫里面插入多條
     */
    @TablePrepare(enableTableShard = true, strategy = SuffixYYYYMMDDHHNameStrategy.class)
    int insertList(@TableShardParam(dependFieldName = "recTime") @Param("list") List<AccHour> list);

    /**
     * 往數(shù)據(jù)庫里面插入多條
     */
    @TablePrepare(enableTableShard = true, strategy = SuffixYYYYMMDDHHNameStrategy.class)
    AccHour selectItem(@TableShardParam(dependFieldName = "recTime") @Param("recvTime") Long recvTime, @Param("pkId") Long pkId);

    /**
     * 查詢指定時間范圍內(nèi)的列表
     *
     * @param startTIme 開始時間
     * @param endTime   解釋時間
     */
    @TablePrepare(enableTableShard = true, strategy = SuffixYYYYMMDDHHNameStrategy.class)
    List<AccHour> selectLIst(@TableShardParam() @Param("startTime") Long startTIme, @Param("endTime") Long endTime);


}
<?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.tuacy.tableshard.mapper.AccHourMapper">

    <!-- 基礎(chǔ)表名 StatisAccHour -->

    <!-- 往數(shù)據(jù)庫里面插入一條記錄 -->
    <insert id="insertItem">

        insert into StatisAccHour (
            `recTime`,
            `ptId`,
            `value`
        ) value (
            #{item.recTime},
            #{item.ptId},
            #{item.value}
        )

    </insert>

    <!-- 批量插入多條記錄 -->
    <insert id="insertList">

        insert into StatisAccHour (
            `recTime`,
            `ptId`,
            `value`
        ) values
         <foreach collection="list" item="item" separator=",">
             (
                #{item.recTime},
                #{item.ptId},
                #{item.value}
             )
         </foreach>

    </insert>

    <!-- 查詢一條記錄 -->
    <select id="selectItem" resultType="com.tuacy.tableshard.entity.model.AccHour">

        select
        `recTime` as recTime,
        `ptId` as ptId,
        `value` as value
        from StatisAccHour
        where recTime = #{recvTime} and ptId = #{pkId}

    </select>

    <!-- 查詢一條記錄 -->
    <select id="selectLIst" resultType="com.tuacy.tableshard.entity.model.AccHour">

        select
        `recTime` as recTime,
        `ptId` as ptId,
        `value` as value
        from StatisAccHour
        where recTime >= ${startTime} and recTime <![CDATA[<=]]> ${endTime}

    </select>

</mapper>

4.5 DAO使用

? ? ??特別要注意,在Dao層我們需要自己保證每一次操作的數(shù)據(jù)庫都是屬于同一個分表的抗悍。比如插入一批數(shù)據(jù)的時候著淆,我們需要自己對不同分表的數(shù)據(jù)做分批次處理因俐。保存每個調(diào)用mapper插入的時候都是屬于同一個分表的數(shù)據(jù)。具體可以看看下面insertList()方法的具體實現(xiàn)。

@Repository
public class AccHourDao extends BaseDao implements IAccHourDao {

    /**
     * 基礎(chǔ)表名
     */
    private static final String BASE_TABLE_NAME = "StatisAccHour";
    /**
     * 分表策略
     */
    private static final ITableNameStrategy STRATEGY = new SuffixYYYYMMDDHHNameStrategy();

    private AccHourMapper accHourMapper;

    @Autowired
    public void setAccHourMapper(AccHourMapper accHourMapper) {
        this.accHourMapper = accHourMapper;
    }

    /**
     * DataSourceAnnotation 用于指定數(shù)據(jù)源,放到統(tǒng)計數(shù)據(jù)庫里面
     */
    @Override
    @DataSourceAnnotation(sourceType = EDataSourceType.STATIS)
    @Transactional(rollbackFor = Exception.class)
    public int insertItem(AccHour item) {
        return accHourMapper.insertItem(item);
    }

    @Override
    @DataSourceAnnotation(sourceType = EDataSourceType.STATIS)
    @Transactional(rollbackFor = Exception.class)
    public int insertList(List<AccHour> list) {
        if (list == null || list.isEmpty()) {
            return 0;
        }
        // 首先,我們不能保證list所有的數(shù)據(jù)都是一張表的,所以我們先得對數(shù)據(jù)分類,按表來分類
        Map<String, List<AccHour>> groupingByTable = list.stream().collect(Collectors.groupingBy(
                item -> STRATEGY.tableName(BASE_TABLE_NAME, item.getRecTime().toString()),
                (Supplier<Map<String, List<AccHour>>>) HashMap::new,
                Collectors.toList()));
        // 遍歷存儲(上面的代碼我們已經(jīng)保存了每個Map.)
        int sucessCount = 0;
        for (List<AccHour> mapValueItem : groupingByTable.values()) {
            sucessCount += accHourMapper.insertList(mapValueItem);
        }
        return sucessCount;
    }

    @Override
    @DataSourceAnnotation(sourceType = EDataSourceType.STATIS)
    @Transactional(rollbackFor = Exception.class)
    public AccHour selectItem(Long recvTime, Long ptId) {
        return accHourMapper.selectItem(recvTime, ptId);
    }

    /**
     * 查詢指定時間范圍的數(shù)據(jù)
     * 針對time做一個簡單說明,
     * 比如當(dāng)前時間是 2019年08月31日00時31分46秒141微妙
     * 則我們在數(shù)據(jù)庫里面存20190831003146141
     *
     * @param startTime 開始時間
     * @param endTime   結(jié)束時間
     * @return 所有查詢到的記錄
     */
    @Override
    @DataSourceAnnotation(sourceType = EDataSourceType.STATIS)
    @Transactional(rollbackFor = Exception.class)
    public List<AccHour> selectList(Long startTime, Long endTime) {
        // long類型是20190831003146141的形式轉(zhuǎn)換為2019年08月31日00時31分46秒141微妙對應(yīng)的LocalDateTime
        LocalDateTime startTimeDate = DbDataTimeUtils.long2DateTime(startTime);
        LocalDateTime endTimeDate = DbDataTimeUtils.long2DateTime(endTime);
        if (startTimeDate.isAfter(endTimeDate)) {
            return null;
        }
        // 數(shù)據(jù)庫里面所有的表
        List<String> allTableName = allTableName();
        if (allTableName == null || allTableName.isEmpty()) {
            return null;
        }
        // 全部轉(zhuǎn)換成小寫
        allTableName = allTableName.stream().map(String::toLowerCase).collect(Collectors.toList());
        List<TwoTuple<Long, Long>> singleTableConditionList = Lists.newArrayList();
        // 我們已經(jīng)確定了當(dāng)前是按小時分表的,表名類似于 StatisAccHour2019083122 的形式,先確定指定的時間范圍里面有多少張表
        while (startTimeDate.isBefore(endTimeDate) || startTimeDate.equals(endTimeDate)) {
            String tableName = STRATEGY.tableName(BASE_TABLE_NAME, String.valueOf(DbDataTimeUtils.dateTime2Long(startTimeDate)));
            if (allTableName.contains(tableName.toLowerCase())) {
                // 有這個表存在
                Long singleTableStartTime = DbDataTimeUtils.dateTime2Long(startTimeDate);
                if (singleTableStartTime < startTime) {
                    singleTableStartTime = startTime;
                }
                singleTableConditionList.add(new TwoTuple<>(singleTableStartTime, endTime));
            }
            startTimeDate = startTimeDate.plusHours(1);
        }
        if (singleTableConditionList.isEmpty()) {
            return null;
        }
        List<AccHour> retList = Lists.newArrayList();
        for (TwoTuple<Long, Long> item : singleTableConditionList) {
            retList.addAll(accHourMapper.selectLIst(item.getFirst(), item.getSecond()));
        }

        return retList;
    }
}

? ? ??關(guān)于Spring Boot Mybatis實現(xiàn)分表功能荞膘,整個的實現(xiàn)邏輯就這么多缩焦。估計上面很多地方我們也沒講明白晰甚,可能有些地方認(rèn)為簡單就沒講卿啡。所以這里面把整個實現(xiàn)源碼的鏈接地址給出來 https://github.com/tuacy/java-study/tree/master/tableshard 推薦大家去看下具體源碼是怎么實現(xiàn)的,有什么疑問可以留言指蚁。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末菩佑,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子凝化,更是在濱河造成了極大的恐慌稍坯,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,744評論 6 502
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件搓劫,死亡現(xiàn)場離奇詭異劣光,居然都是意外死亡袜蚕,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,505評論 3 392
  • 文/潘曉璐 我一進店門绢涡,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人遣疯,你說我怎么就攤上這事雄可。” “怎么了缠犀?”我有些...
    開封第一講書人閱讀 163,105評論 0 353
  • 文/不壞的土叔 我叫張陵数苫,是天一觀的道長。 經(jīng)常有香客問我辨液,道長虐急,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,242評論 1 292
  • 正文 為了忘掉前任滔迈,我火速辦了婚禮止吁,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘燎悍。我一直安慰自己敬惦,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,269評論 6 389
  • 文/花漫 我一把揭開白布谈山。 她就那樣靜靜地躺著俄删,像睡著了一般。 火紅的嫁衣襯著肌膚如雪奏路。 梳的紋絲不亂的頭發(fā)上畴椰,一...
    開封第一講書人閱讀 51,215評論 1 299
  • 那天,我揣著相機與錄音鸽粉,去河邊找鬼斜脂。 笑死,一個胖子當(dāng)著我的面吹牛潜叛,可吹牛的內(nèi)容都是我干的秽褒。 我是一名探鬼主播,決...
    沈念sama閱讀 40,096評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼威兜,長吁一口氣:“原來是場噩夢啊……” “哼销斟!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起椒舵,我...
    開封第一講書人閱讀 38,939評論 0 274
  • 序言:老撾萬榮一對情侶失蹤蚂踊,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后笔宿,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體犁钟,經(jīng)...
    沈念sama閱讀 45,354評論 1 311
  • 正文 獨居荒郊野嶺守林人離奇死亡棱诱,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,573評論 2 333
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了涝动。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片迈勋。...
    茶點故事閱讀 39,745評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖醋粟,靈堂內(nèi)的尸體忽然破棺而出靡菇,到底是詐尸還是另有隱情,我是刑警寧澤米愿,帶...
    沈念sama閱讀 35,448評論 5 344
  • 正文 年R本政府宣布厦凤,位于F島的核電站,受9級特大地震影響育苟,放射性物質(zhì)發(fā)生泄漏较鼓。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,048評論 3 327
  • 文/蒙蒙 一违柏、第九天 我趴在偏房一處隱蔽的房頂上張望博烂。 院中可真熱鬧,春花似錦勇垛、人聲如沸脖母。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,683評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽谆级。三九已至,卻和暖如春讼积,著一層夾襖步出監(jiān)牢的瞬間肥照,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,838評論 1 269
  • 我被黑心中介騙來泰國打工勤众, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留舆绎,地道東北人。 一個月前我還...
    沈念sama閱讀 47,776評論 2 369
  • 正文 我出身青樓们颜,卻偏偏與公主長得像吕朵,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子窥突,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,652評論 2 354

推薦閱讀更多精彩內(nèi)容