作者:京東保險 王奕龍
物流的分揀業(yè)務(wù)在某些分揀場地只有一個數(shù)據(jù)源,因為數(shù)據(jù)量比較大,將所有數(shù)據(jù)存在一張表內(nèi)查詢速度慢,也為了做不同設(shè)備數(shù)據(jù)的分庫管理,便在這個數(shù)據(jù)源內(nèi)創(chuàng)建了多個不同庫名但表完全相同的數(shù)據(jù)庫
現(xiàn)在需要上線報表服務(wù)來查詢所有數(shù)據(jù)庫中的數(shù)據(jù)進行統(tǒng)計,那么現(xiàn)在的問題來了,該如何 滿足在配置一個數(shù)據(jù)源的情況下來查詢該數(shù)據(jù)源下不同數(shù)據(jù)庫的數(shù)據(jù) 呢,借助搜索引擎查到的分庫實現(xiàn)大多是借助 Sharding-JDBC 框架,配置多個數(shù)據(jù)源根據(jù)分庫算法實現(xiàn)數(shù)據(jù)源的切換,但是對于只有一個數(shù)據(jù)源的系統(tǒng)來說,我覺得引入框架再將單個數(shù)據(jù)源根據(jù)不同的庫名配置成多個不同的數(shù)據(jù)源來實現(xiàn)分庫查詢的邏輯我覺得并不好。
如果我們能在 SQL 執(zhí)行前將 SQL 中所有的表名前拼接上對應(yīng)的庫名的話,那么就能夠?qū)崿F(xiàn)數(shù)據(jù)源的切換了,下面我們講一下使用 JSqlParser 和 Mybatis攔截器 實現(xiàn)該邏輯,借助 JSqlParser 主要是為了解析SQL,找到其中所有的表名進行拼接,如果大家有更好的實現(xiàn)方式,該組件并不是必須的。
實現(xiàn)邏輯
SqlSource 是讀取 XML 中 SQL 內(nèi)容并將其發(fā)送給數(shù)據(jù)庫執(zhí)行的對象,如果我們在執(zhí)行前能攔截到該對象,并將其中的 SQL 替換掉便達成了我們的目的。 SqlSource 有多種實現(xiàn),包括常見的DynamicSqlSource。其中包含著必要的執(zhí)行邏輯,我們需要做的工作便是在這些邏輯執(zhí)行完之后,對 SQL 進行改造,所以這次實現(xiàn)我們使用了 裝飾器模式,在原來的 SqlSource 上套一層,執(zhí)行完 SqlSource 本身的方法之后對其進行增強,代碼如下:
public abstract class AbstractDBNameInterceptor {
/**
* SqlSource 的裝飾器,作用是增強了 getBoundSql 方法,在基礎(chǔ)上增加了動態(tài)分庫的邏輯
*/
static class SqlSourceDecorator implements SqlSource {
/**
* SQL 字段名稱
*/
private static final String SQL_FIELD_NAME = "sql";
/**
* 原本的 sql source
*/
private final SqlSource sqlSource;
/**
* 裝飾器進行封裝
*/
public SqlSourceDecorator(SqlSource sqlSource) {
this.sqlSource = sqlSource;
}
@Override
public BoundSql getBoundSql(Object parameterObject) {
try {
// 先生成出未修改前的 SQL
BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
// 獲取數(shù)據(jù)庫名
String dbName = getSpecificDBName(parameterObject);
// 有效才修改
if (isValid(dbName)) {
// 生成需要修改完庫名的 SQL
String targetSQL = getRequiredSqlWithSpecificDBName(boundSql, dbName);
// 更新 SQL
updateSql(boundSql, targetSQL);
}
return boundSql;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* 校驗是否為有效庫名
*/
private boolean isValid(String dbName) {
return StringUtils.isNotEmpty(dbName) && !"null".equals(dbName);
}
/**
* 獲取到我們想要的庫名的 SQL
*/
private String getRequiredSqlWithSpecificDBName(BoundSql boundSql, String dbName) throws JSQLParserException {
String originSql = boundSql.getSql();
// 獲取所有的表名
Set tables = TablesNamesFinder.findTables(originSql);
for (String table : tables) {
originSql = originSql.replaceAll(table, dbName + "." + table);
}
return originSql;
}
/**
* 修改 SQL
*/
private void updateSql(BoundSql boundSql, String sql) throws NoSuchFieldException, IllegalAccessException {
// 通過反射修改sql語句
Field field = boundSql.getClass().getDeclaredField(SQL_FIELD_NAME);
field.setAccessible(true);
field.set(boundSql, sql);
}
}
// ...
}
定義了 AbstractDBNameInterceptor 抽象類是為了實現(xiàn)復(fù)用,并將 SqlSourceDecorator 裝飾器定義為靜態(tài)內(nèi)部類,這樣的話,將所有邏輯都封裝在抽象類內(nèi)部,之后這部分實現(xiàn)好后研發(fā)直接實現(xiàn)抽象類的通用方法即可,不必關(guān)注它的內(nèi)部實現(xiàn)。
結(jié)合注釋我們解釋一下 SqlSourceDecorator 的邏輯,其中用到了 Java 反射相關(guān)的操作。首先通過反射獲取到 SQL,getSpecificDBName 方法是需要自定義實現(xiàn)的,其中 parameterObject 對象是傳到 DAO 層執(zhí)行查詢時的參數(shù),在我們的業(yè)務(wù)中是能夠根據(jù)其中的設(shè)備相關(guān)參數(shù)拿到對應(yīng)的所在庫名的,而設(shè)備和具體庫名的映射關(guān)系需要提前初始化好。在獲取到具體的庫名后執(zhí)行 getRequiredSqlWithSpecificDBName 方法來將其拼接到表名前,在這里我們使用到了 JSqlParser 的工具類,解析出來所有的表名,執(zhí)行字符串的替換,最后一步同樣是使用反射操作將該參數(shù)值再寫回去,這樣便完成了指定庫名的任務(wù)。
接下來我們需要看下抽象攔截器中供攔截器復(fù)用的方法,如下:
public abstract class AbstractDBNameInterceptor {
/**
* SqlSource 字段名稱
*/
private static final String SQL_SOURCE_FIELD_NAME = "sqlSource";
/**
* 執(zhí)行修改數(shù)據(jù)庫名的邏輯
*/
protected Object updateDBName(Invocation invocation) throws Throwable {
// 裝飾器裝飾 SqlSource
decorateSqlSource((MappedStatement) invocation.getArgs()[0]);
return invocation.proceed();
}
/**
* 裝飾 SqlSource
*/
private void decorateSqlSource(MappedStatement statement) throws NoSuchFieldException, IllegalAccessException {
if (!(statement.getSqlSource() instanceof SqlSourceDecorator)) {
Field sqlSource = statement.getClass().getDeclaredField(SQL_SOURCE_FIELD_NAME);
sqlSource.setAccessible(true);
sqlSource.set(statement, new SqlSourceDecorator(statement.getSqlSource()));
}
}
}
這個還是比較簡單的,只是借助反射機制做了一層“裝飾”,查詢攔截器實現(xiàn)如下:
@Intercepts({
@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 SelectDBNameInterceptor extends AbstractDBNameInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
return updateDBName(invocation);
}
}
將其配置到 Mybatis 攔截器中,便能實現(xiàn)數(shù)據(jù)庫動態(tài)切換了。
審核編輯 黃宇
-
數(shù)據(jù)庫
+關(guān)注
關(guān)注
7文章
4020瀏覽量
68342 -
mybatis
+關(guān)注
關(guān)注
0文章
64瀏覽量
7139
發(fā)布評論請先 登錄
lanbview怎么與數(shù)據(jù)庫連接?
LabView動態(tài)創(chuàng)建數(shù)據(jù)源的方法
LabVIEW連接Access數(shù)據(jù)庫的問題
QuickBI助你成為分析師——搞定數(shù)據(jù)源
springboo修改數(shù)據(jù)源為Druid
數(shù)據(jù)源配置工具
數(shù)據(jù)倉庫入門之創(chuàng)建數(shù)據(jù)源
基于Mybatis攔截器實現(xiàn)數(shù)據(jù)范圍權(quán)限
如何實現(xiàn)基于Mybatis攔截器實現(xiàn)數(shù)據(jù)范圍權(quán)限呢?
多數(shù)據(jù)源數(shù)據(jù)轉(zhuǎn)換和同步的ETL工具推薦
SpringBoot實現(xiàn)動態(tài)切換數(shù)據(jù)源
Mybatis 攔截器實現(xiàn)單數(shù)據(jù)源內(nèi)多數(shù)據(jù)庫切換
評論