dbVisitor 是一個旨在提供統一數據庫訪問體驗的 Java 工具庫。隨着對 MySQL、PostgreSQL 等關係型數據庫以及 MongoDB、ElasticSearch 等 NoSQL 數據源支持的不斷深入,底層的方言系統(Dialect System)面臨着越來越複雜的挑戰。
近期,我們對 dbVisitor 的方言系統進行了一次深度的架構重構。本次重構不涉及功能變更,旨在解決舊架構中存在的抽象割裂問題,將"方言元數據"與"命令構建能力"高度內聚。
本文將深入探討這次架構演進背後的思考、實施方案以及帶來的顯著優勢。
背景:舊架構的痛點
在重構之前,dbVisitor 的方言層設計採用了職責分離的原則,主要由兩個平行的接口體系構成:
SqlDialect:負責定義數據庫的靜態特徵和元數據。例如:左右轉義符、關鍵字集合、分頁語句的拼接模式、表名/列名的格式化規則等。它通常是無狀態的單例。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())
- MySQL 場景:
- 中間類冗餘 :為了適配 NoSQL,我們引入了
MongoBuilderDialect這樣的膠水代碼,僅僅是為了把 Dialect 和 Builder 粘合在一起,這增加了代碼庫的複雜度。
演進:方言即工廠
本次重構的核心理念是:方言對象本身應該是構建器的工廠。
如果説 SqlDialect 定義了數據庫"是什麼"(元數據),那麼由它生產的 CommandBuilder 實例就負責解決"怎麼做"(構建查詢)。
核心變更
-
引入工廠方法 : 我們在
SqlDialect(及其子接口/抽象類)中引入了newBuilder()方法。任何一個方言實現,都必須有能力創建一個能理解該方言的構建器。 -
原型模式: 我們將 Dialect 實現類賦予了"雙重身份":
- 作為元數據對象 (單例):如
MySqlDialect.DEFAULT,無狀態,提供關鍵字定義等通用信息。 - 作為構建器對象 (原型):當調用
MySqlDialect.DEFAULT.newBuilder()時,它會返回一個新的MySqlDialect實例(或者專門的內部類實例),這個新實例持有查詢狀態(table, where, columns...)。
// 重構後的 MySqlDialect 簡略示意 public class MySqlDialect extends AbstractSqlDialect { // 元數據定義... @Override public SqlCommandBuilder newBuilder() { // 返回一個新的實例,用於構建 SQL return new MySqlDialect(); } } - 作為元數據對象 (單例):如
-
繼承體系重組與簡化 : 我們徹底移除了獨立的
SqlCommandBuilder類文件,將其邏輯下沉到了抽象基類中。新的層級結構如下:AbstractBuilderDialect: 頂層基類,定義通用的 Builder 行為。AbstractSqlDialect: (核心) 繼承自前者,實現了標準 JDBC SQL 的生成邏輯(SELECT/UPDATE/INSERT...)。所有標準 SQL 數據庫(MySQL, PG, Oracle 等)均繼承此基類。MongoDialect: 直接繼承自AbstractBuilderDialect,內部實現了針對 MongoDB BSON 的構建邏輯。徹底移除了舊版本中的MongoCommandBuilder和MongoBuilderDialect。AbstractElasticDialect: 為 ES 提供 DSL 構建支持。
{{{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