> ## Documentation Index
> Fetch the complete documentation index at: https://java.agentscope.io/llms.txt
> Use this file to discover all available pages before exploring further.

# JDBC

`agentscope-extensions-jdbc` 提供基于标准 JDBC 的全链路分布式存储实现，是关系型数据库的统一入口：传入任意 JDBC `DataSource`，方言通过 SPI 自动检测，无需针对数据库单独选型。适合已有关系型数据库基础设施的场景。

目前已支持的数据库：

| 数据库 | 定位 |
| - | - |
| MySQL | 生产环境常用选项 |
| PostgreSQL | 生产环境常用选项 |
| H2 | 内存 / 嵌入式，测试与开发 |
| SQLite | 嵌入式，轻量单机场景 |

后续将持续扩展对更多关系型数据库的支持，包括 Oracle 以及达梦、高斯、OceanBase 等国产数据库。

> 历史模块 `agentscope-extensions-mysql`、`agentscope-extensions-postgresql`、`agentscope-extensions-skill-mysql-repository`、`agentscope-extensions-skill-postgresql-repository` 已废弃，由本模块统一取代，迁移方式见下文。

## 添加依赖

```xml theme={null}
<dependency>
    <groupId>io.agentscope</groupId>
    <artifactId>agentscope-extensions-jdbc</artifactId>
    <version>${agentscope.version}</version>
</dependency>
```

数据库驱动按实际使用自行引入（如 `mysql-connector-j`、`postgresql`、`sqlite-jdbc`）。

## 一键配置

```java theme={null}
import io.agentscope.extensions.jdbc.JdbcDistributedStore;

DataSource dataSource = ...;  // HikariCP, Druid, etc.
DistributedStore store = JdbcDistributedStore.create(dataSource);

HarnessAgent agent = HarnessAgent.builder()
    .distributedStore(store)
    .filesystem(new RemoteFilesystemSpec()
            .isolationScope(IsolationScope.USER))
    .build();
```

自定义表名推荐只通过 `tablePrefix` 设置统一前缀；builder 另提供 `storeTableName` / `sessionStateTableName` / `snapshotTableName` 支持逐表完全自定义，通常无需使用：

```java theme={null}
AbstractJdbcDialect dialect = AbstractJdbcDialect.from(dataSource)
    .tablePrefix("myapp_")            // 默认 agentscope_
    .build();

DistributedStore store = JdbcDistributedStore.create(dataSource, dialect);
```

建表与结构校验统一在 `AbstractJdbcDialect.from(ds).build()` 装配时完成：`autoCreateTable`（默认 true）控制是否执行 DDL，无论开关与否都会校验已启用表组的业务表，缺表或缺列即带参考 DDL 快速失败（Spring 环境在启动时暴露）。表名与前缀仅允许 `[A-Za-z_][A-Za-z0-9_]*`。

**表组开关**：`enableBaseTables`（默认开）与 `enableSkillTables`（默认关）决定 `build()` 建表与校验覆盖哪些表组，与 `autoCreateTable` 正交、彼此也独立——基础表用 MySQL、skill 走 git 通道是合法组合。

```java theme={null}
AbstractJdbcDialect dialect = AbstractJdbcDialect.from(dataSource)
    .enableBaseTables(true)      // 默认开 —— store、sessions、snapshots
    .enableSkillTables(true)     // 默认关 —— 显式开启才建 skill 表
    .build();                    // 会创建 agentscope_skills / agentscope_skill_resources
```

默认创建的表：`agentscope_store`、`agentscope_sessions`、`agentscope_snapshots`；锁表 `agentscope_distributed_locks` 在首次加锁时创建。开启 skill 表组后额外创建：`agentscope_skills`、`agentscope_skill_resources`。

## 提供的组件

### 1. JdbcAgentStateStore

Agent 状态持久化到数据库表。

```java theme={null}
import io.agentscope.extensions.jdbc.dialect.AbstractJdbcDialect;
import io.agentscope.extensions.jdbc.state.JdbcAgentStateStore;

AbstractJdbcDialect dialect = AbstractJdbcDialect.from(dataSource).build();  // 建表与校验在此完成

AgentStateStore store = new JdbcAgentStateStore(dataSource, dialect);
```

**表结构**：自动创建的表包含 `session_id`、`state_key`、`item_index`、`state_data`（LONGTEXT JSON）、`version`、`created_at`、`updated_at` 等列，主键为 `(session_id, state_key, item_index)`。`version` 列支撑 `saveIfVersion` 乐观并发控制（CAS 写入）。

### 2. JdbcStore（BaseStore）

工作区文件系统 KV 存储，SQL 全部来自方言层，组件本身与数据库无关。

```java theme={null}
import io.agentscope.extensions.jdbc.store.JdbcStore;

BaseStore store = JdbcStore.builder(dataSource)
    .dialect(dialect)
    .build();
```

**并发安全**：`putIfVersion` 通过单语句 CAS `UPDATE ... WHERE version = ?` 实现，所有已支持数据库均可用。

### 3. JdbcSnapshotSpec

沙箱快照以 BLOB 存储到数据库表（MySQL 为 LONGBLOB，PostgreSQL 为 BYTEA）。

```java theme={null}
import io.agentscope.extensions.jdbc.snapshot.JdbcSnapshotSpec;

SandboxSnapshotSpec spec = new JdbcSnapshotSpec(dataSource, dialect);
```

### 4. JdbcSandboxExecutionGuard

分布式锁，锁策略由方言决定：

* **MySQL**：原生 `GET_LOCK()` / `RELEASE_LOCK()`，锁绑定 JDBC 连接，连接关闭时自动释放；lock name 超过 64 字符时自动 hash。
* **PostgreSQL / H2 / SQLite**：基于 `agentscope_distributed_locks` 表的通用锁，不依赖数据库私有语法。

```java theme={null}
import io.agentscope.extensions.jdbc.sandbox.JdbcSandboxExecutionGuard;

SandboxExecutionGuard guard = JdbcSandboxExecutionGuard.builder(dialect)
    .keyPrefix("myapp:lock:")
    .lockTimeout(Duration.ofMinutes(30))
    .build();
```

> 注意：MySQL named locks 是 server 级别的（非 database 级别）。在共享 MySQL 实例时，使用唯一的 `keyPrefix` 避免冲突。

### 5. JdbcAgentSkillRepository

skill 存储走同一套方言：实现 core 的 `AgentSkillRepository`，本模块支持的每个数据库都能直接作为 skill 通道。

```java theme={null}
import io.agentscope.core.skill.repository.AgentSkillRepository;
import io.agentscope.extensions.jdbc.skill.JdbcAgentSkillRepository;

AbstractJdbcDialect dialect = AbstractJdbcDialect.from(dataSource)
    .enableSkillTables(true)
    .build();

AgentSkillRepository repo = new JdbcAgentSkillRepository(dataSource, dialect);
```

建两张表：`agentscope_skills` 和 `agentscope_skill_resources`（复合主键 `(id, resource_path)`，外键 `ON DELETE CASCADE`）。与其他组件一样，repository 不碰 schema——表组在 build 时开启；方言没开表组就直接构造，会立刻报错。

两点行为说明：

* `metadata_json` 是必需列。早于该列的旧表会在启动校验时报错并附参考 DDL，按提示 `ALTER TABLE` 补列后重启即可，框架不会代改已有表。
* `delete` 会先显式删掉技能的资源行再删技能行，因此在 SQLite（默认不启用级联外键）上行为也一致；资源路径必须是相对路径且不含 `..`，越出技能目录的路径在保存时即被拒绝，读取回表中的行时也做同样校验。

#### 作用域隔离（namespace）

技能名跨团队重名是常态，`agentscope_skills` 用 `namespace` 列做作用域隔离：唯一键为 **`UNIQUE(namespace, name)`**，namespace 内唯一、跨 namespace 可重名。`skill_resources` 同样带 `namespace` 列，与技能行同一事务写入。这也是 `AgentSkillRepository` 作用域隔离契约在关系型存储上的落地：**一个实例绑定一个 namespace，无参方法全部作用于绑定值**。

```sql theme={null}
namespace VARCHAR(64) NOT NULL DEFAULT 'default'   -- 存量行自动落入 default
CONSTRAINT uk_namespace_name UNIQUE (namespace, name)
```

```java theme={null}
JdbcAgentSkillRepository teamA =
    new JdbcAgentSkillRepository(dataSource, dialect, "team-a", true);
JdbcAgentSkillRepository teamB =
    new JdbcAgentSkillRepository(dataSource, dialect, "team-b", true);

teamA.save(List.of(skill), false);                 // 团队 A 保存
teamB.save(List.of(skill), false);                 // 同名技能在另一 namespace，互不影响
teamA.getSkill("code-review");
teamA.delete("code-review");                       // 只作用于绑定的 namespace

// 同一实例上显式指定 namespace 的便捷方法
teamA.save("team-b", List.of(otherSkill), false);
```

* 不指定 namespace 的构造器都绑定 `"default"`。
* `force=true` 的覆盖只发生在同一 namespace 内，不会跨 namespace 删旧写新。
* namespace 在构造与便捷方法入口都校验：1–64 字符、`[A-Za-z0-9._-]`，非法抛 `IllegalArgumentException`。
* namespace 比较在所有数据库上均大小写敏感；MySQL 下两表的 `namespace` 列显式指定 `utf8mb4_bin`，`name` 列保持默认的大小写不敏感。
* `clearAllSkills()` 清除一个 namespace 的全部技能（无参形式作用于绑定的 namespace）。
* `namespace` **一次写入**——仓库没有任何语句会更新它。

**存量升级**（框架不会代改已有表，需人工执行一次）：

```sql theme={null}
-- 1. 加列（瞬时完成，存量行自动归入 default；两张表都要）
ALTER TABLE agentscope_skills
  ADD COLUMN namespace VARCHAR(64) NOT NULL DEFAULT 'default';
ALTER TABLE agentscope_skill_resources
  ADD COLUMN namespace VARCHAR(64) NOT NULL DEFAULT 'default';

-- 2. 仅 MySQL——把隔离边界固定为大小写敏感比较（会重建表，建议低峰执行）
ALTER TABLE agentscope_skills
  MODIFY COLUMN namespace VARCHAR(64) COLLATE utf8mb4_bin NOT NULL DEFAULT 'default';
ALTER TABLE agentscope_skill_resources
  MODIFY COLUMN namespace VARCHAR(64) COLLATE utf8mb4_bin NOT NULL DEFAULT 'default';

-- 3. 重建唯一索引（建议低峰执行：先删旧的单列唯一，再建复合唯一），
--    并为资源表的 namespace 建索引
ALTER TABLE agentscope_skills DROP INDEX name;
ALTER TABLE agentscope_skills
  ADD UNIQUE KEY uk_namespace_name (namespace, name);
ALTER TABLE agentscope_skill_resources ADD INDEX idx_namespace (namespace);
```

PostgreSQL 变体：

```sql theme={null}
ALTER TABLE agentscope_skills ADD COLUMN namespace VARCHAR(64) NOT NULL DEFAULT 'default';
ALTER TABLE agentscope_skill_resources ADD COLUMN namespace VARCHAR(64) NOT NULL DEFAULT 'default';
ALTER TABLE agentscope_skills DROP CONSTRAINT agentscope_skills_name_key;
ALTER TABLE agentscope_skills ADD CONSTRAINT uk_namespace_name UNIQUE (namespace, name);
CREATE INDEX IF NOT EXISTS agentscope_skill_resources_namespace_idx
  ON agentscope_skill_resources (namespace);
```

H2 的旧唯一约束可以按名字 `ALTER TABLE agentscope_skills DROP CONSTRAINT <约束名>` 后重建。SQLite 不同：内联 `UNIQUE` 产生的隐式索引删不掉，需要按"建新表 → 拷数据 → 删旧表 → 改名"的标准表重建流程处理（两张表都在建新表时写好 `namespace` 列，skills 表同时写 `UNIQUE(namespace, name)`）。

启动校验只比对列不比对索引，**第 2、3 步都要做**（第 3 步拆除全局 `UNIQUE(name)`；第 2 步使 MySQL 下 namespace 比较大小写敏感）。资源表索引仅影响性能。

## 从历史模块迁移

历史模块不建议继续使用，请按自身节奏尽早迁移：

| 已废弃 | 替代 |
| - | - |
| `MysqlDistributedStore.create(ds)` / `PostgresDistributedStore.create(ds)` | `JdbcDistributedStore.create(ds)` |
| `new MysqlAgentStateStore(ds)` | `new JdbcAgentStateStore(ds, AbstractJdbcDialect.from(ds).build())` |
| `JdbcStore.builder(ds).dialect(mysqlDialect)` | `JdbcStore.builder(ds).dialect(AbstractJdbcDialect.from(ds).build())` |
| `MysqlSkillRepository` / `PostgresSkillRepository`（skill-mysql / skill-postgresql 模块） | `new JdbcAgentSkillRepository(ds, AbstractJdbcDialect.from(ds).enableSkillTables(true).build())` |

skill 仓库迁移要点：

* 现行旧模块建的表已包含 `metadata_json`，原样可用；更早的旧表先补上这一列，启动报错里附有参考 DDL。
* "原样可用"有一个例外：旧模块从不拒绝绝对路径或含 `..` 的资源路径，而新实现对读取的行做同样校验——`getSkill` 会拒绝这类行，`getAllSkills` / `getAllSkillNames` 会跳过并告警。名字本身非法的行也无法通过 API 删除，只能 `clearAllSkills` 或直接 SQL 处理。迁移前请先审计 `resource_path` 和技能名；这些值本就无法被下游安全消费。
* 旧模块会隐式创建 `agentscope` 库（MySQL）/ schema（PostgreSQL）；新实现的表放在连接所指向的库里——把 `DataSource` 指向存量表即可。
* `databaseName` / `schemaName` 无对应物——表跟随 DataSource 所指向的库，与基础表一致。表名可用 `skillTableName` / `skillResourcesTableName` 覆盖。相应地，`getSource()` 由 `mysql_<库名>_<表名>` / `postgresql_<schema>_<表名>` 变为 `jdbc_<skillTableName>@<namespace>`（namespace 后缀保证同一张表的不同作用域互不混淆）：以其为键的消费方（如 skill 暂存缓存命名空间）迁移后会使用新的子目录，旧目录由孤儿 GC 回收。

## 选型建议

| 场景 | 建议 |
| - | - |
| 已有关系型数据库，不想引入 Redis | **首选** JDBC 模块 |
| 需要 SQL 审计 / 报表 / 联表查询 | JDBC 模块 |
| 快照数据量大（>100MB） | BLOB 可行但推荐 OSS |
| 追求最低延迟 | Redis |


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.