方言系统架构演进:从分离到统一

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...)。
    java 复制代码
    // 重构后的 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 构建支持。

改造后的优势

1. 极简且安全的 API

对于上层调用者(如 LambdaTemplate),获取构建器变得异常统一和简单。再也不需要 instanceof 判断,也不需要在构建时传入 Dialect 参数:

java 复制代码
// 旧方式(伪代码):逻辑分散且冗余
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() 方法。

相关推荐
Darren2455 分钟前
Junit到Springboot单元测试
后端
张涛酱1074568 分钟前
「实战」Spring Boot 4.1.0-M3 新特性速览:gRPC、OpenTelemetry全面升级
后端
龙码精神9 分钟前
ClickHouse 容灾技术方案(两方案对比+落地细节)
后端·架构
bugcome_com9 分钟前
WPF 命令 ICommand 从原理到实战
后端·wpf·icommand
若水不如远方11 分钟前
分布式一致性(七):架构角度 —— 分布式共识系统的选型指南
分布式·后端
shark_chili13 分钟前
Spring 核心知识点全面解析
后端
WavesMan16 分钟前
YoBFF 实战复盘:让审计日志从“可用”走向“可运营
后端
SimonKing21 分钟前
企微、QQ统统接入OpenClaw,蓄水池已满,准备养虾
java·后端·程序员
CodeSheep28 分钟前
王自如公开招聘01号员工,这要求有多离谱?
前端·后端·程序员
洛阳泰山31 分钟前
我用 Java 21 虚拟线程重写了一个 RAG 平台:从架构设计到踩坑实录
java·人工智能·后端