? ? ??開發(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)的,有什么疑問可以留言指蚁。