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