agentscope-ai / agentscope-ai/agentscope-java
[Feature]: 多数据库抽象架构:统一 mysql(jdbc) 扩展模块各组件的方言设计(agentscope-extensions-mysql -> agentscope-extensions-jdbc)
- 主要語言
- Java
- 星號
- 5.6k
- 分支
- 1.3k
- 平均合併
- 4 天 12 小時
- 30 天內合併 PR
- 77
描述
## 背景
`agentscope-extensions-mysql` 模块提供四个 JDBC 组件,由门面类 `MysqlDistributedStore` 组装:
| 组件 | 实现类 | 实现的 SPI |
| ---------------- | --------------------------- | --------------------------------- |
| 会话状态存储 | `MysqlAgentStateStore` | `AgentStateStore` (core) |
| 文件系统 KV 存储 | `JdbcStore` | `BaseStore` (harness) |
| 沙箱快照 | `JdbcRemoteSnapshotClient` | `RemoteSnapshotClient` (harness) |
| 分布式锁 | `JdbcSandboxExecutionGuard` | `SandboxExecutionGuard` (harness) |
此外,项目中还存在一个平行的 `agentscope-extensions-postgresql` 模块,以相同的四组件结构(`PostgresDistributedStore` / `PostgresAgentStateStore` / `PostgresBaseStore` / `PostgresRemoteSnapshotClient` / `PostgresSandboxExecutionGuard`)独立实现了同一套功能。两个模块之间没有共享的抽象层。
## 问题陈述
### 核心问题:多数据库方向的架构只完成了四分之一
这个模块在**基础架构上已经选择了多数据库方向**——`store` 包中的 `JdbcStoreDialect` 接口及其四个实现(MySQL / PostgreSQL / SQLite / H2)就是明确的证据。`JdbcStoreDialect.from(DataSource)` 甚至实现了基于 `DatabaseMetaData` 的自动探测,用户传入任意 JDBC `DataSource` 即可工作。
这说明设计的**初衷就是通用的 JDBC 存储**,而非 MySQL 专属。
但这个抽象架构**只落地到了四个组件中的一个**。其余三个组件完全没有沿用这套模式:
| 组件 | 是否方言化 | 抽象状态 |
| --------------------------- | :--------: | -------------------------------------------------------- |
| `JdbcStore` | ✅ | 完整的 `JdbcStoreDialect` 接口 + 4 个方言实现 + 自动探测 |
| `MysqlAgentStateStore` | ❌ | MySQL SQL 硬编码在业务逻辑中,无抽象层 |
| `JdbcRemoteSnapshotClient` | ❌ | MySQL SQL 硬编码,无抽象层 |
| `JdbcSandboxExecutionGuard` | ❌ | MySQL `GET_LOCK()` 硬编码,无抽象层 |
这是**架构层面的不一致**,不是个别类的疏忽。
### 具体表现
**1. SQL 与业务逻辑耦合**
以 `MysqlAgentStateStore`(860 行)为例,MySQL 专属语法直接内联在 save/get/delete 等业务方法中:
- `ON DUPLICATE KEY UPDATE`(UPSERT 语法)
- `INFORMATION_SCHEMA.SCHEMATA / TABLES`(元数据查询)
- backtick 标识符转义(`` `db`.`table` ``)
- `utf8mb4` / `LONGTEXT` / `TRUNCATE TABLE`(MySQL 专属 DDL/DML)
这些 SQL 散落在业务方法里,无法被整体替换。`JdbcRemoteSnapshotClient`(`LONGBLOB` + `ON DUPLICATE KEY`)和 `JdbcSandboxExecutionGuard`(`GET_LOCK / RELEASE_LOCK`)同理。
相比之下,`JdbcStore` 的业务逻辑(CAS、namespace 编码、分页搜索)完全数据库无关,SQL 全部委托给 `JdbcStoreDialect`——这才是正确的架构姿态。
**2. "部分支持"陷阱**
由于四个组件的数据库耦合度不同,用户通过 `JdbcStore`(已方言化)接入 PostgreSQL 后,会合理推断整个模块可用。但 `AgentStateStore` 和 `SandboxExecutionGuard` 会在运行时因 MySQL 专属 SQL 失败。
这种"一个组件支持、三个组件不支持"的混合状态,比"完全不支持"更危险——前者制造虚假信心,后者至少边界清晰。
**3. 无统一抽象层,扩展无章可循**
四个组件各自为政,没有将方言聚合在一起的顶层设计。新增数据库支持需要在三个不同包里分别寻找并修改硬编码 SQL,且没有 `store` 包那样的接口指引"应该实现什么"。
**4. 现有方言代码存在冗余抽象方法**
即使在被认为最成熟的 `store` 包中,`JdbcStoreDialect` 的抽象方法也存在冗余:`getInsertSql()` 和 `getCasUpdateSql()` 在全部四个方言实现中**逐字相同**,但每个方言类都各自复制粘贴了一份。这些本应是接口的 default 方法。
**5. 模块名与包名是半成品抽象的历史遗留**
模块名 `mysql` 和包名 `io.agentscope.extensions.mysql` 是"最初只支持 MySQL"时期的产物。随着 `store` 包引入多库方言,这个命名已经与实际架构方向脱节——`PostgresJdbcStoreDialect` 出现在 `mysql.store` 包下就是直接的结构信号。命名问题是抽象不完整的**症状**,不是根因。
**6. 缺乏共享抽象已导致整模块级重复**
`agentscope-extensions-postgresql` 模块的存在是上述所有问题的**最终后果**。因为四个组件没有统一的方言抽象层,支持 PostgreSQL 的唯一途径是复制整个模块——`PostgresAgentStateStore`、`PostgresBaseStore`、`PostgresRemoteSnapshotClient`、`PostgresSandboxExecutionGuard`,每个类都是 mysql 模块对应类的平行重写。
更矛盾的是:mysql 模块的 `store` 包里**已经有** `PostgresJdbcStoreDialect`,而 postgresql 模块里又有独立的 `PostgresBaseStore`——同一种数据库的 KV 存储存在两套实现,用户无从选择。如果方言抽象在最初就覆盖了全部四个组件,PostgreSQL 支持只需一个 `PostgresDialect` 类,整个 postgresql 模块就不会存在。
## 提议
**补全已经开始但未完成的多数据库抽象架构**,通过接口继承 + default 方法的设计,使四个组件的方言覆盖 depth 一致,并让新增数据库只需实现一个聚合类、覆盖有限的差异化方法。
## 目标架构
自上而下分为四层:入口层、组件层(数据库无关业务逻辑)、方言接口层(继承聚合 + default)、数据库实现层(每库一个类,只覆盖差异)。
```
┌───────────────────────────────────────────────────────────────────────────┐
│ 入口层 │
│ │
│ DataSource │
│ (MySQL / PG / Oracle / H2 / SQLite) │
│ │ │
│ ▼ │
│ ┌───────────────────────────┐ │
│ │ JdbcDistributedStore │ │
│ │ (门面) │ │
│ │ │ │
│ │ from(ds) → 自动探测方言 │ │
│ └──┬──────┬──────┬────┬─────┘ │
└──────────────────────┼──────┼──────┼────┼──────────────────────────────────┘
│ │ │ │
┌──────────┘ │ │ └──────────┐
▼ ▼ ▼ ▼
┌───────────────────────────────────────────────────────────────────────────┐
│ 组件层 (数据库无关业务逻辑,实现外部 SPI) │
│ │
│ ┌───────────────┐ ┌───────────┐ ┌───────────────┐ ┌───────────────┐ │
│ │JdbcAgent │ │ │ │JdbcSnapshot │ │JdbcSandbox │ │
│ │StateStore │ │ JdbcStore │ │Spec → │ │ExecutionGuard │ │
│ │ │ │ │ │JdbcRemote │ │ │ │
│ │ implements │ │ implements│ │SnapshotClient │ │ implements │ │
│ │ AgentState │ │ BaseStore │ │ │ │ SandboxExec │ │
│ │ Store │ │ │ │ implements │ │ Guard │ │
│ │ │ │ │ │ RemoteSnapshot│ │ │ │
│ │ │ │ │ │ Client │ │ │ │
│ └──────┬────────┘ └─────┬─────┘ └───────┬───────┘ └───────┬───────┘ │
│ │ │ │ │ │
└─────────┼─────────────────┼────────────────┼──────────────────┼───────────┘
│ SQL 委托 │ SQL 委托 │ SQL 委托 │ 锁策略委托 │
└─────────────────┴───────┬────────┴──────────────────┘ │
│ │
▼ ▼
┌───────────────────────────────────────────────────────────────────────────┐
│ 方言接口层 (继承聚合 + default 方法) │
│ │
│ ┌──────────────┐ ┌────────────────────┐ ┌──────────────────────┐ │
│ │JdbcStore │ │AgentStateStore │ │SnapshotStore │ │
│ │Dialect │ │Dialect │ │Dialect │ │
│ │ │ │ │ │ │ │
│ │ 抽象(2): │ │ 抽象(3): │ │ 抽象(1): │ │
│ │ createTable │ │ createSessionsTbl │ │ upsertSnapshotSql │ │
│ │ upsertSql │ │ upsertStateSql │ │ │ │
│ │ │ │ checkTableExists │ │ │ │
│ │ default(6): │ │ │ │ default(4): │ │
│ │ insertSql │ │ default(5+): │ │ blobType = BLOB │ │
│ │ casUpdateSql│ │ insertStateSql │ │ createTableSql │ │
│ │ selectSql │ │ selectStateSql │ │ insertSnapshotSql │ │
│ │ deleteSql │ │ deleteStateSql │ │ selectSnapshotSql │ │
│ │ searchSql │ │ quoteIdentifier │ │ │ │
│ │ likeEscape │ │ = ANSI "..." │ │ │ │
│ │ │ │ createDatabaseSql │ │ │ │
│ │ │ │ = null (跳过) │ │ │ │
│ └──────┬───────┘ └─────────┬──────────┘ └──────────┬───────────┘ │
│ │ │ │ │
│ └────────────┬────────┴─────────────────────────┘ │
│ │ extends │
│ ▼ │
│ ┌───────────────────────┐ │
│ │ JdbcDialect │ ◄── static from(DataSource) │
│ │ (聚合接口) │ 自动探测 │
│ │ │ │
│ │ default: │ │
│ │ lockStrategy(ds) │ │
│ │ → TableBasedLock │ (通用表锁降级,任何 JDBC 库可用) │
│ └──────────┬────────────┘ │
└──────────────────────┼───────────────────────────────────────────────────┘
│ implements
▼
┌───────────────────────────────────────────────────────────────────────────┐
│ 数据库方言实现层 (每库一个类,只覆盖差异) │
│ │
│ ┌────────────┐ ┌──────────────┐ ┌───────────────┐ ┌───────────────┐ │
│ │MysqlDialect │ │PostgresDialect│ │OracleDialect │ │ H2Dialect │ │
│ │ 覆盖 ~4 │ │ 覆盖 ~0 │ │ (新增) ~6 │ │ 覆盖 ~0-1 │ │
│ │ │ │ │ │ │ │ │ │
│ │ •建表 │ │ ANSI 基准 │ │ •建表 VARCHAR2│ │ (几乎全继承 │ │
│ │ LONGTEXT │ │ 全部继承 │ │ /CLOB/NUMBER │ │ default) │ │
│ │ ENGINE │ │ default │ │ •UPSERT MERGE │ │ │ │
│ │ •UPSERT │ │ │ │ •checkTable │ │ │ │
│ │ ON DUPL.. │ │ │ │ ALL_TABLES │ │ │ │
│ │ •quote(`) │ │ │ │ •快照 UPSERT │ │ │ │
│ │ •createDB │ │ │ │ •lock DBMS_ │ │ │ │
│ │ •lock │ │ │ │ │ │ │ │
│ │ GET_LOCK │ │ │ │ │ │ │ │
│ └────────────┘ └──────────────┘ └───────────────┘ └───────────────┘ │
│ │
│ ┌───────────────┐ │
│ │SqliteDialect │ 覆盖 ~2: 建表(TEXT/INTEGER) + checkTable │
│ └───────────────┘ │
└───────────────────────────────────────────────────────────────────────────┘
```
**图例**
| 标记 | 含义 |
| ------------ | ------------------------------------------------------------------------------ |
| `抽象(N)` | N 个抽象方法——数据库实现类**可能**需要覆盖的差异点 |
| `default(N)` | N 个 default 方法——以 ANSI/PostgreSQL SQL 为基准,实现类**自动继承**,无需编写 |
| `覆盖 ~N` | 该数据库方言实际需要 override 的方法数量(越小越接近 ANSI 标准) |
| `SQL 委托` | 组件类不持有任何 SQL 字符串,全部从 dialect 接口获取 |
| `implements` | Java `implements`,数据库方言类实现 `JdbcDialect` 聚合接口 |
**关键设计约束**
- **组件层零 SQL**:`JdbcStore`、`JdbcAgentStateStore` 等组件类的业务逻辑(CAS、namespace 编码、hash 变更检测、增量追加、事务管理)完全数据库无关,SQL 全部委托给 dialect。
- **接口零冗余**:所有数据库相同的 SQL(INSERT、CAS UPDATE 等)只存在于 default 方法中,没有方言类重复实现。
- **新增数据库 = 一个类**:实现 `JdbcDialect` 即可,编译器强制覆盖所有抽象方法,default 方法自动继承。不需要创建多个文件,不需要理解组件间的组装关系。
## 设计思想
### 1. 推广已验证的 dialect 模式,以 ANSI SQL 为 default 基准
`store` 包的 `JdbcStoreDialect` 是已被证明有效的范本。进一步观察可以发现,真正因数据库而异的只有两类 SQL:
- **UPSERT 语法**:`ON DUPLICATE KEY`(MySQL)/ `ON CONFLICT`(PG、SQLite、H2 2.x)/ `MERGE INTO`(Oracle、旧版 H2)
- **DDL 类型与选项**:`LONGTEXT` vs `TEXT` vs `CLOB`、`ENGINE=InnoDB`、`utf8mb4` 等
其余 SQL(INSERT、CAS UPDATE、SELECT、DELETE、分页搜索)要么在所有数据库中完全相同,要么遵循 ANSI 标准。这些应全部作为接口的 **default 方法**,以 ANSI / PostgreSQL 语法为基准。
这样做的直接收益:**现有的四个方言实现中,`getInsertSql()` 和 `getCasUpdateSql()` 可以提升为 default 方法,立即消除冗余**。
### 2. 三个组件级方言接口,各以 default 方法最大化通用 SQL
每条线保持独立的方言接口,操作各自不同的表结构,方法名按领域命名以避免冲突:
- `JdbcStoreDialect`(已有,优化 default 覆盖率)——操作 KV 存储表
- `AgentStateStoreDialect`(新增)——操作 sessions 表,default 覆盖 INSERT/SELECT/DELETE/标识符转义(ANSI 双引号)/建库(默认跳过)
- `SnapshotStoreDialect`(新增)——操作 snapshots 表,default 覆盖 BLOB 类型(默认 `BLOB`)/INSERT/SELECT/EXISTS
每个接口中,**只有真正因库而异的方法保持抽象**,其余全部 default。
### 3. 聚合接口 `JdbcDialect` 继承三个子接口
```java
public interface JdbcDialect extends JdbcStoreDialect, AgentStateStoreDialect, SnapshotStoreDialect {
// 锁策略:默认提供基于锁表的可移植降级方案(SELECT ... FOR UPDATE)
// MySQL / PG / Oracle 等有原生 advisory lock 的数据库覆盖此方法
default SandboxLockStrategy lockStrategy(DataSource dataSource) {
return new TableBasedLockStrategy(dataSource);
}
static JdbcDialect from(DataSource dataSource) { /* 自动探测 */ }
}
```
新增一种数据库 = **实现这一个 `JdbcDialect` 接口**,编译器会强制覆盖所有抽象方法,default 方法自动继承。不需要创建多个文件,不需要理解组件间的组装关系。
### 4. SandboxExecutionGuard 的锁策略作为 default 方法
数据库锁机制是**行为差异**而非语法差异(MySQL `GET_LOCK` / PG `pg_advisory_lock` / Oracle `DBMS_LOCK`),不适合 SQL 模板抽象。将其作为 `JdbcDialect` 的 default 方法提供表锁降级方案(基于 `SELECT ... FOR UPDATE` 的锁表,任何 JDBC 数据库可用),有原生锁的数据库覆盖即可。
### 5. 诚实标注能力矩阵
补全抽象后,不是所有数据库的原生能力都相同。应明确标注各数据库对四个组件的支持程度,对短板(如 SQLite 无原生 advisory lock)由 default 方法提供降级。
## 新增数据库的工作量示例
以新增 Oracle 为例,实现 `JdbcDialect` 只需覆盖有限的差异化方法:
| 来源接口 | 需要覆盖的方法 | 原因 | 可继承的 default |
| ------------------------ | ----------------------------------------------------------------------------- | ------------------------------ | ---------------------------------------------------- |
| `JdbcStoreDialect` | `getCreateTableSql()`, `getUpsertSql()` | VARCHAR2/CLOB 类型、MERGE 语法 | INSERT、CAS、SELECT、DELETE、搜索 |
| `AgentStateStoreDialect` | `getCreateSessionsTableSql()`, `getUpsertStateSql()`, `checkTableExistsSql()` | 类型差异、MERGE、`ALL_TABLES` | INSERT/SELECT/DELETE、标识符双引号、建库跳过 |
| `SnapshotStoreDialect` | `getUpsertSnapshotSql()` | MERGE 语法 | BLOB 类型(Oracle 原生即 BLOB)、建表、INSERT/SELECT |
| `JdbcDialect` | `lockStrategy()` | DBMS_LOCK(可选) | 表锁降级(不覆盖也可用) |
**Oracle 总计覆盖 ~6 个方法,写入一个类,约 80 行。** 其余全部继承 default。各数据库 override 量:PostgreSQL 接近 0(ANSI 基准)、SQLite 约 2、MySQL 约 4、Oracle 约 6。
## 收益
- **架构一致性**:四个组件的抽象深度对齐,消除"一个方言化、三个硬编码"的割裂。
- **扩展极简**:新增数据库只需实现一个 `JdbcDialect` 类,覆盖有限的差异方法,default 方法自动继承。编译器保证不遗漏。
- **零冗余**:ANSI/标准 SQL 作为 default 方法,消除当前方言类中的重复代码(如 `getInsertSql` 四份相同实现)。
- **消除"部分支持"陷阱**:接入某数据库时,能力边界由 dialect 接口显式定义,而非运行时踩雷。
- **命名归位**:模块名与包名跟随抽象的完成自然更正,而非生硬改名。
## 实施策略
以现有 `agentscope-extensions-mysql` 模块为参照,**完全重构实现一个全新的 `agentscope-extensions-jdbc` 模块**。不对 `mysql` 和 `postgresql` 模块做任何修改——它们作为参照源和过渡期可用版本保持原样。
jdbc 模块发布时,同时标记 `agentscope-extensions-mysql` 和 `agentscope-extensions-postgresql` 为 `@Deprecated`,Javadoc 指向新模块作为替代。现有用户按自身节奏迁移,旧模块在过渡期内行为不变。后续大版本移除两个 deprecated 模块。
这种方式的优点是:现有用户零风险(旧模块完全不动),新模块无历史包袱(greenfield 实现,不必维护兼容薄壳),迁移节奏由用户掌控。
## 涉及范围
**全新 `agentscope-extensions-jdbc` 模块(新建,不改动现有模块):**
- 方言接口层:三个子接口(`JdbcStoreDialect`、`AgentStateStoreDialect`、`SnapshotStoreDialect`)+ 聚合接口 `JdbcDialect`,均以 ANSI SQL 为 default 基准
- 组件层:四个数据库无关的组件实现(参考现有 mysql 模块的业务逻辑,如 CAS、namespace 编码、hash 变更检测、增量追加等,在新模块中重新实现)
- 数据库实现层:`MysqlDialect`、`PostgresDialect` 等方言类(PostgresDialect 天然替代 postgresql 模块的全部六个类)
- 门面类:`JdbcDistributedStore`
**对现有模块的操作(仅标记,不修改代码):**
- 标记 `agentscope-extensions-mysql` 为 `@Deprecated`
- 标记 `agentscope-extensions-postgresql` 为 `@Deprecated`
## 总结
可以详细查看上文的 “架构图”,结构性、设计可扩展性都做了充分的考虑。新增一个数据库的实现将变成非常清晰简单。
---
请作者查看架构图方案,如果可以采纳新的架构设计方案。我可以参与这个新的模块 `agentscope-extensions-jdbc` 的重构工作贡献PR。
盼复,谢谢。
貢獻指南
評估
這個 Issue 還沒有評估資料。