dbVisitor 方言系統架構演進:從分離到統一

新聞
HongKong
9
10:39 AM · Jan 16 ,2026

dbVisitor 是一個旨在提供統一數據庫訪問體驗的 Java 工具庫。隨着對 MySQL、PostgreSQL 等關係型數據庫以及 MongoDB、ElasticSearch 等 NoSQL 數據源支持的不斷深入,底層的方言系統(Dialect System)面臨着越來越複雜的挑戰。

近期,我們對 dbVisitor 的方言系統進行了一次深度的架構重構。本次重構不涉及功能變更,旨在解決舊架構中存在的抽象割裂問題,將"方言元數據"與"命令構建能力"高度內聚。

本文將深入探討這次架構演進背後的思考、實施方案以及帶來的顯著優勢。

背景:舊架構的痛點

在重構之前,dbVisitor 的方言層設計採用了職責分離的原則,主要由兩個平行的接口體系構成:

  1. SqlDialect :負責定義數據庫的靜態特徵和元數據。例如:左右轉義符、關鍵字集合、分頁語句的拼接模式、表名/列名的格式化規則等。它通常是無狀態的單例
  2. SqlCommandBuilder (及其子類 MongoCommandBuilder 等):負責動態構建查詢命令。它持有查詢的上下文(SELECT 哪些列、WHERE 條件是什麼),最終生成 BoundSql。它是有狀態的對象。

存在的問題

這種分離雖然遵循了單一職責原則,但在實際擴展和維護中暴露出了明顯的問題:

  • 抽象割裂 :當我們要適配一種新數據庫(例如 TiDB)時,實現一個 TiDBDialect 很容易,但如果它的 SQL 語法比較特殊,我們可能需要修改通用的 SqlCommandBuilder 甚至繼承一個新的 Builder。對於 MongoDB 這種非 SQL 數據源,情況更糟:我們需要創建特定的 MongoCommandBuilder,並且必須在上層代碼(如 LambdaTemplate)中硬編碼判斷邏輯來決定實例化哪個 Builder。
  • API 使用繁瑣 :用户或上層框架在構建查詢時,必須顯式地進行"配對"。
    • MySQL 場景:new SqlCommandBuilder(new MySqlDialect())
    • Mongo 場景:new MongoCommandBuilder(new MongoDialect())
  • 中間類冗餘 :為了適配 NoSQL,我們引入了 MongoBuilderDialect 這樣的膠水代碼,僅僅是為了把 Dialect 和 Builder 粘合在一起,這增加了代碼庫的複雜度。

演進:方言即工廠

本次重構的核心理念是:方言對象本身應該是構建器的工廠

如果説 SqlDialect 定義了數據庫"是什麼"(元數據),那麼由它生產的 CommandBuilder 實例就負責解決"怎麼做"(構建查詢)。

核心變更

  1. 引入工廠方法 : 我們在 SqlDialect(及其子接口/抽象類)中引入了 newBuilder() 方法。任何一個方言實現,都必須有能力創建一個能理解該方言的構建器。

  2. 原型模式: 我們將 Dialect 實現類賦予了"雙重身份":

    • 作為元數據對象 (單例):如 MySqlDialect.DEFAULT,無狀態,提供關鍵字定義等通用信息。
    • 作為構建器對象 (原型):當調用 MySqlDialect.DEFAULT.newBuilder() 時,它會返回一個新的 MySqlDialect 實例(或者專門的內部類實例),這個新實例持有查詢狀態(table, where, columns...)。
    // 重構後的 MySqlDialect 簡略示意
    public class MySqlDialect extends AbstractSqlDialect {
        // 元數據定義...
    
        @Override
        public SqlCommandBuilder newBuilder() {
            // 返回一個新的實例,用於構建 SQL
            return new MySqlDialect(); 
        }
    }
    
  3. 繼承體系重組與簡化 : 我們徹底移除了獨立的 SqlCommandBuilder 類文件,將其邏輯下沉到了抽象基類中。新的層級結構如下:

    • AbstractBuilderDialect: 頂層基類,定義通用的 Builder 行為。
    • AbstractSqlDialect: (核心) 繼承自前者,實現了標準 JDBC SQL 的生成邏輯(SELECT/UPDATE/INSERT...)。所有標準 SQL 數據庫(MySQL, PG, Oracle 等)均繼承此基類。
    • MongoDialect: 直接繼承自 AbstractBuilderDialect,內部實現了針對 MongoDB BSON 的構建邏輯。徹底移除了舊版本中的 MongoCommandBuilderMongoBuilderDialect
    • AbstractElasticDialect: 為 ES 提供 DSL 構建支持。

QQ20260115-213454.png{{{width="auto" height="auto"}}}

改造後的優勢

1. 極簡且安全的 API

對於上層調用者(如 LambdaTemplate),獲取構建器變得異常統一和簡單。再也不需要 instanceof 判斷,也不需要在構建時傳入 Dialect 參數:

// 舊方式(偽代碼):邏輯分散且冗餘
CommandBuilder builder;
if (dialect instanceof MongoDialect) {
    builder = new MongoCommandBuilder();
} else {
    builder = new SqlCommandBuilder();
}
// 需要顯式關聯,甚至在 build 時還要再次傳入,存在不匹配風險
BoundSql sql = builder.buildSelect(dialect, true); 

// 新方式:統一多態,自包含
CommandBuilder builder = dialect.newBuilder();
// 構建器本身就是 Dialect 的一種形態,無需再傳入參數,杜絕了"張冠李戴"
BoundSql sql = builder.buildSelect(true); 

2. 內聚性提升

所有的數據庫特定邏輯------無論是"轉義符是什麼"還是"如何生成 INSERT 語句"------現在都收斂在同一個類(或其父類)中。 例如,MongoDialect 現在是一個自包含的單元,它既知道 Mongo 的關鍵字,也知道如何生成 Mongo 查詢。

3. 消除了方言不匹配的風險

在舊版本中,SqlCommandBuilder 在生成 SQL 時要求傳入 SqlDialect 對象。這在 API 設計上留下了隱患:理論上,你可以創建一個 MongoCommandBuilder 卻傳給它一個 MySqlDialect,這會導致運行時錯誤或荒謬的查詢構建。 重構後,構建器由方言直接生產,並且 buildSelect 等方法不再接收 Dialect 參數。構建器"自帶"元數據知識,從編譯層面杜絕了方言不匹配的可能。

4. 代碼量減少與維護性提高

通過這次重構,我們刪除了多個冗餘的 Builder 類和適配器類。 測試用例也變得更加通用:我們可以編寫一套針對 dialect.newBuilder() 的測試,然後用不同的 Dialect 實現去運行它,只需驗證生成的 BoundSql 字符串即可。

升級指南

對於 dbVisitor 的普通使用者,本次重構是完全透明的,API 保持向下兼容。 對於開發自定義 Dialect 的高階用户,如果您之前依賴了 SqlCommandBuilder 類,請將其改為繼承 AbstractSqlDialect 並重寫 newBuilder() 方法。

  • 項目首頁:https://www.dbvisitor.net/
  • 項目源碼:https://gitee.com/zycgit/dbvisitor
  • 原文:https://www.dbvisitor.net/blog/dbvisitor-dialect-refactoring
user avatar
0 位用戶收藏了這個故事!
收藏

發佈 評論

Some HTML is okay.