CloudBeaver 26.2.3 API 使用详解
CloudBeaver 26.2.3 GraphQL API 实测详解:/api/gql 单端点、103 Query/84 Mutation 操作分布与 BadFaithIntrospection 内省保护、authLogin 会话认证、连接全生命周期管理、异步 SQL 执行任务链、导航树元数据与 DDL 获取、CSV 导出下载,以及口令错误与异步并发乱序排障实录。
CloudBeaver 26.2.3 API 使用详解
目标:把 CloudBeaver Community 26.2.3 的 GraphQL API 按「总览 → 认证 → 连接管理 → SQL 执行 → 元数据 → 数据导出」的顺序完整走一遍,所有命令与回显均在本机实测(CloudBeaver Community 26.2.3.202610190842、Ubuntu 24.04.5),取材日期 2026-10-25。部署与账号准备见《CloudBeaver 26.2.3 Docker Compose 部署指南》,会话超时、口令策略等服务端参数见《CloudBeaver 26.2.3 配置参数详解》。
一、环境信息
- 应用版本:CloudBeaver Community
26.2.3(构建号26.2.3.202610190842) - API 端点:
http://127.0.0.1:8978/api/gql(GraphQL over HTTP POST) - 演示账号:管理员
root/q1w2e3r4 - 演示连接:
pg-demo(PostgreSQL 16,库demo)、maria-demo(MariaDB 11.8,库demo),演示表sensor_log - 调用工具:
curl+ 一个 10 行的python3封装(见 2.3 节,用于避免 SQL 引号在 shell 里的转义问题)
二、API 总览
2.1 单端点与 GraphQL 模型
CloudBeaver 的全部前后端交互都走同一个端点 POST /api/gql,请求体是标准 GraphQL JSON:{"query": "..."}。与常见 GraphQL 服务不同,CloudBeaver 把几乎所有操作都声明为 Query,包括登录 authLogin、建用户 createUser、改密码 setUserCredentials 等「写操作」;真正的数据读写(建连接、执行 SQL)才放在 Mutation 里。因此调用时不能只凭语义猜 query 还是 mutation,写错了会收到 FieldUndefined 校验错误。
2.2 操作分布:103 个 Query 与 84 个 Mutation(实测计数)
通过内省分别统计两类根操作的字段数:
curl -s -b ck.txt -H 'Content-Type: application/json' http://127.0.0.1:8978/api/gql \
-d '{"query":"{ __schema { queryType { fields { name } } } }"}' -o q.json
curl -s -b ck.txt -H 'Content-Type: application/json' http://127.0.0.1:8978/api/gql \
-d '{"query":"{ __schema { mutationType { fields { name } } } }"}' -o m.json
python3 -c "import json; print('queryType fields:', len(json.load(open('q.json'))['data']['__schema']['queryType']['fields'])); print('mutationType fields:', len(json.load(open('m.json'))['data']['__schema']['mutationType']['fields']))"
实测回显:
queryType fields: 103
mutationType fields: 84
常用操作速查(按域分组,均实测可调用):
| 域 | 操作 | 类型 |
|---|---|---|
| 认证 | authLogin / authLogoutExtended / sessionState | Query |
| 服务信息 | serverConfig / sessionPermissions | Query |
| 用户管理 | listUsers / createUser / setUserCredentials | Query |
| 连接 | userConnections / connectionInfo / driverList | Query |
| 连接 | createConnection / testConnection / initConnection / closeConnection / deleteConnection | Mutation |
| SQL | asyncSqlExecuteQuery / asyncSqlExecuteResults / sqlContextCreate / sqlContextDestroy | Mutation |
| 任务 | asyncTaskInfo / asyncTaskCancel | Mutation |
| 元数据 | navNodeChildren / navNodeInfo / metadataGetNodeDDL | Query |
| 导出 | dataTransferExportDataFromContainer / dataTransferAvailableStreamProcessors | Query |
2.3 会话机制:cb-session-id Cookie 与匿名会话
认证方式是服务端会话:authLogin 成功后,响应头 Set-Cookie: cb-session-id=<会话串>; HttpOnly 种下 Cookie,后续所有请求携带该 Cookie 即视为已登录,没有 Bearer Token 机制。因为实例开启了匿名访问(anonymousAccessEnabled: true),不带 Cookie 的请求也会自动获得一个匿名会话——可以调用公共接口,但看不到任何用户连接,调用管理接口会被拒绝(见 8.1 节)。
SQL 里大量单引号在 shell 里转义易错,本文用一个最小封装复用 Cookie 并打印美化后的响应,文件 gql.py 内容如下(实测使用的就是它):
import json, sys, urllib.request, http.cookiejar
cj = http.cookiejar.MozillaCookieJar('ck.txt')
cj.load(ignore_discard=True, ignore_expires=True)
op = urllib.request.build_opener(urllib.request.HTTPCookieProcessor(cj))
def gql(query):
req = urllib.request.Request('http://127.0.0.1:8978/api/gql', data=json.dumps({'query': query}).encode(), headers={'Content-Type': 'application/json'})
return json.dumps(json.loads(op.open(req).read()), indent=2, ensure_ascii=False)
if __name__ == '__main__':
print(gql(sys.argv[1]))
用法:python3 gql.py '<GraphQL 查询文本>'。
2.4 内省保护:BadFaithIntrospection
服务带有「恶意内省」拦截:单个请求里 __Type.fields 出现次数过多会被直接拒绝。把 2.2 节的两个统计合并成一次请求即触发:
curl -s -b ck.txt -H 'Content-Type: application/json' http://127.0.0.1:8978/api/gql \
-d '{"query":"{ __schema { queryType { fields { name } } mutationType { fields { name } } } }"}'
实测回显:
{
"errors": [
{
"message": "This request is not asking for introspection in good faith - __Type.fields is present too often!",
"extensions": {
"classification": "BadFaithIntrospection"
}
}
]
}
实用结论:批量抓取 schema 时要拆成多次小请求,这也是 2.2 节分两次统计的原因。
三、认证与首次调用
3.1 免登录探测 serverConfig
serverConfig 不需要登录即可调用,适合部署后冒烟验证与版本确认:
curl -s -H 'Content-Type: application/json' http://127.0.0.1:8978/api/gql \
-d '{"query":"query { serverConfig { name version productInfo { name id version buildTime } workspaceId anonymousAccessEnabled supportsCustomConnections enabledAuthProviders defaultNavigatorSettings { showSystemObjects showUtilityObjects } } }"}'
实测回显:
{
"data": {
"serverConfig": {
"name": "CloudBeaver Lab",
"version": "26.2.3.202610190842",
"productInfo": {
"name": "CloudBeaver Community",
"id": "io.cloudbeaver.product.ce.product",
"version": "26.2.3.202610190842",
"buildTime": "October 19, 2026"
},
"workspaceId": "DHNATZNLMUVBV",
"anonymousAccessEnabled": true,
"supportsCustomConnections": false,
"enabledAuthProviders": [
"local"
],
"defaultNavigatorSettings": {
"showSystemObjects": false,
"showUtilityObjects": false
}
}
}
}
一次调用同时确认了版本、构建时间、工作区 ID、匿名访问开关与可用认证方式(local 即内置口令认证)。
3.2 authLogin 登录
注意 authLogin 是 Query。用 curl -c ck.txt 把会话 Cookie 落盘,供后续 -b ck.txt 复用:
curl -s -c ck.txt -H 'Content-Type: application/json' http://127.0.0.1:8978/api/gql \
-d '{"query":"query { authLogin(provider: \"local\", credentials: { user: \"root\", password: \"q1w2e3r4\" }) { userTokens { userId displayName loginTime } } }"}'
实测回显:
{
"data": {
"authLogin": {
"userTokens": [
{
"userId": "root",
"displayName": "root",
"loginTime": "2026-10-25T19:25:27.222+08:00"
}
]
}
}
}
落盘的 Cookie 文件内容(grep cb-session-id ck.txt 实测回显):
#HttpOnly_127.0.0.1 FALSE / FALSE 1792931127 cb-session-id l0xl9dtgmgzy12afx12noym458
3.3 会话状态查询与登出
sessionState 查看当前会话:
python3 gql.py 'query { sessionState { createTime lastAccessTime cacheExpired valid } }'
实测回显:
{
"data": {
"sessionState": {
"createTime": "2026-10-25T11:25:27Z",
"lastAccessTime": "2026-10-25T11:25:27Z",
"cacheExpired": false,
"valid": true
}
}
}
登出操作叫 authLogoutExtended(同样是 Query,不是 authLogout——后者在 schema 里不存在,会直接报 FieldUndefined):
python3 gql.py 'query { authLogoutExtended { redirectLinks { url sameTabRedirect } } }'
实测回显:
{
"data": {
"authLogoutExtended": {
"redirectLinks": []
}
}
}
登出后服务端把会话降级为匿名会话:sessionState.valid 仍为 true(匿名会话也是合法会话),但 userConnections 返回空数组,管理接口返回 accessDenied(见 8.1 节)。本文后续演示前已重新执行 3.2 节登录。
四、连接管理域
4.1 列出连接 userConnections
curl -s -b ck.txt -H 'Content-Type: application/json' http://127.0.0.1:8978/api/gql \
-d '{"query":"query { userConnections { id name driverId host port databaseName connected } }"}'
实测回显(服务重建后连接仍在,说明连接定义持久化在工作区,与会话无关):
{
"data": {
"userConnections": [
{
"id": "postgres-jdbc-1a172a8859d-66a0bb96dac402dc",
"name": "pg-demo",
"driverId": "postgresql:postgres-jdbc",
"host": "pg",
"port": "5432",
"databaseName": "demo",
"connected": false
},
{
"id": "mariaDB-1a172ab0e54-d5008b42ec7a307",
"name": "maria-demo",
"driverId": "mysql:mariaDB",
"host": "maria",
"port": "3306",
"databaseName": "demo",
"connected": false
}
]
}
}
连接 id 由「驱动短名-时间戳-哈希」组成,是后续所有连接级操作的入参。connected: false 表示定义存在但尚未建立 JDBC 连接。
4.2 测试连接 testConnection(错误口令排障)
testConnection 用一份临时配置试连,不落库。故意给错误口令:
curl -s -b ck.txt -H 'Content-Type: application/json' http://127.0.0.1:8978/api/gql \
-d '{"query":"mutation { testConnection(config: { driverId: \"postgresql:postgres-jdbc\", host: \"pg\", port: \"5432\", databaseName: \"demo\", credentials: { userName: \"postgres\", userPassword: \"wrong-pass\" } }) { id connected connectionError { message errorCode } } }"}'
实测回显(错误经 GraphQL errors 数组返回,extensions 里带 Java 异常类、SQLState 与完整堆栈):
{
"errors": [
{
"message": "Error connecting to database:\nConnection failed:\nFATAL: password authentication failed for user \"postgres\"",
"locations": [
{
"line": 1,
"column": 12
}
],
"path": [
"testConnection"
],
"extensions": {
"exceptionClass": "org.postgresql.util.PSQLException",
"stackTrace": "org.postgresql.util.PSQLException: FATAL: password authentication failed for user \"postgres\"\n\tat org.postgresql.Driver$ConnectTask.getResult(Driver.java:399)\n\tat org.postgresql.Driver.connect(Driver.java:311)\n\tat org.jkiss.dbeaver.model.impl.jdbc.JDBCConnectionOpener.run(JDBCConnectionOpener.java:112)\n\tat org.jkiss.dbeaver.model.impl.jdbc.JDBCConnectionOpener.run(JDBCConnectionOpener.java:86)\n\tat org.jkiss.dbeaver.model.impl.jdbc.JDBCDataSource.openConnection(JDBCDataSource.java:262)\n\tat org.jkiss.dbeaver.ext.postgresql.model.PostgreDataSource.openConnection(PostgreDataSource.java:591)\n\tat org.jkiss.dbeaver.ext.postgresql.model.PostgreDataSource.openConnection(PostgreDataSource.java:522)\n\tat org.jkiss.dbeaver.model.impl.jdbc.JDBCExecutionContext.connect(JDBCExecutionContext.java:114)\n\tat org.jkiss.dbeaver.model.impl.jdbc.JDBCRemoteInstance.lambda$initializeMainContext$0(JDBCRemoteInstance.java:106)\n\tat org.jkiss.dbeaver.model.exec.DBExecUtils.tryOpenContext(DBExecUtils.java:1176)\n\tat org.jkiss.dbeaver.model.impl.jdbc.JDBCRemoteInstance.initializeMainContext(JDBCRemoteInstance.java:105)\n\tat org.jkiss.dbeaver.ext.postgresql.model.PostgreDatabase.checkInstanceConnection(PostgreDatabase.java:236)\n\tat org.jkiss.dbeaver.ext.postgresql.model.PostgreDatabase.<init>(PostgreDatabase.java:121)\n\tat org.jkiss.dbeaver.ext.postgresql.model.PostgreDataSource.createDatabaseImpl(PostgreDataSource.java:264)\n\tat org.jkiss.dbeaver.ext.postgresql.model.PostgreDataSource.initializeRemoteInstance(PostgreDataSource.java:158)\n\tat org.jkiss.dbeaver.model.impl.jdbc.JDBCDataSource.<init>(JDBCDataSource.java:109)\n\tat org.jkiss.dbeaver.model.impl.jdbc.JDBCDataSource.<init>(JDBCDataSource.java:100)\n\tat org.jkiss.dbeaver.ext.postgresql.model.PostgreDataSource.<init>(PostgreDataSource.java:95)\n\tat org.jkiss.dbeaver.ext.postgresql.PostgreDataSourceProvider.openDataSource(PostgreDataSourceProvider.java:128)\n\tat org.jkiss.dbeaver.ext.postgresql.PostgreDataSourceProvider.openDataSource(PostgreDataSourceProvider.java:45)\n\tat org.jkiss.dbeaver.registry.DataSourceDescriptor.openDataSource(DataSourceDescriptor.java:1409)\n\tat org.jkiss.dbeaver.registry.DataSourceDescriptor.connect0(DataSourceDescriptor.java:1205)\n\tat org.jkiss.dbeaver.registry.DataSourceDescriptor.connect(DataSourceDescriptor.java:1060)\n\tat org.jkiss.dbeaver.runtime.jobs.ConnectJob.run(ConnectJob.java:76)\n\tat org.jkiss.dbeaver.runtime.jobs.ConnectionTestJob.run(ConnectionTestJob.java:104)\n\tat io.cloudbeaver.service.core.impl.WebServiceCore.testConnection(WebServiceCore.java:591)\n\tat java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)\n\tat java.base/java.lang.reflect.Method.invoke(Method.java:565)",
"errorCode": 0,
"databaseState": "28P01",
"classification": "DataFetchingException"
}
}
],
"data": null
}
排障要点:message 三行结构固定为「CloudBeaver 包装信息 → 连接阶段 → 驱动原始报错」;databaseState: "28P01" 即 PostgreSQL 的口令认证失败 SQLState;exceptionClass 直接指出是 JDBC 驱动抛出的异常,说明问题在目标库而非 CloudBeaver 本身。
4.3 建立连接 initConnection
对已保存的连接发起真正的 JDBC 连接。saveCredentials: false 的连接需在此传 credentials:
curl -s -b ck.txt -H 'Content-Type: application/json' http://127.0.0.1:8978/api/gql \
-d '{"query":"mutation { initConnection(id: \"postgres-jdbc-1a172a8859d-66a0bb96dac402dc\", credentials: { userName: \"postgres\", userPassword: \"q1w2e3r4\" }) { id name connected connectTime serverVersion connectionError { message } } }"}'
实测回显:
{
"data": {
"initConnection": {
"id": "postgres-jdbc-1a172a8859d-66a0bb96dac402dc",
"name": "pg-demo",
"connected": true,
"connectTime": "2026-10-25T11:26:47Z",
"serverVersion": null,
"connectionError": null
}
}
}
4.4 创建连接 createConnection
createConnection 把连接定义持久化到工作区。驱动 ID 可用 driverList 查询(CE 26.2.3 预下载 18 个驱动,清单见《CloudBeaver 26.2.3 配置参数详解》第十章)。创建一个 MariaDB 连接:
python3 gql.py 'mutation { createConnection(config: { name: "maria-api", driverId: "mysql:mariaDB", host: "maria", port: "3306", databaseName: "demo", credentials: { userName: "root", userPassword: "q1w2e3r4" }, saveCredentials: true }) { id name driverId host port connected } }'
实测回显:
{
"data": {
"createConnection": {
"id": "mariaDB-1a172d5362b-6da56f019a098507",
"name": "maria-api",
"driverId": "mysql:mariaDB",
"host": "maria",
"port": "3306",
"connected": false
}
}
}
saveCredentials: true 把口令一并保存,之后 initConnection 无需再传凭据:
python3 gql.py 'mutation { initConnection(id: "mariaDB-1a172d5362b-6da56f019a098507") { id name connected connectTime } }'
实测回显:
{
"data": {
"initConnection": {
"id": "mariaDB-1a172d5362b-6da56f019a098507",
"name": "maria-api",
"connected": true,
"connectTime": "2026-10-25T11:32:27Z"
}
}
}
顺手在该连接上验证 MariaDB 可用(执行流程见第五章):
{
"data": {
"asyncSqlExecuteResults": {
"statusMessage": "Executed",
"duration": 8,
"results": [
{
"resultSet": {
"columns": [
{
"name": "ver",
"typeName": "VARCHAR"
}
],
"rowsWithMetaData": [
{
"data": [
"11.8.9-MariaDB-ubu2404"
]
}
]
}
}
]
}
}
}
4.5 关闭与删除 closeConnection / deleteConnection
关闭只断开 JDBC 会话,删除才移除工作区里的定义:
python3 gql.py 'mutation { closeConnection(id: "mariaDB-1a172d5362b-6da56f019a098507") { id name connected } }'
实测回显:
{
"data": {
"closeConnection": {
"id": "mariaDB-1a172d5362b-6da56f019a098507",
"name": "maria-api",
"connected": false
}
}
}
python3 gql.py 'mutation { deleteConnection(id: "mariaDB-1a172d5362b-6da56f019a098507") }'
实测回显:
{
"data": {
"deleteConnection": true
}
}
再次 userConnections 确认只剩 pg-demo 与 maria-demo,maria-api 已被移除。
五、SQL 执行域(异步任务模型)
5.1 创建 SQL 上下文 sqlContextCreate
SQL 执行不是无状态的:先在某个已建立的连接上创建 SQL 上下文(可指定默认 catalog/schema,相当于会话内的当前库):
curl -s -b ck.txt -H 'Content-Type: application/json' http://127.0.0.1:8978/api/gql \
-d '{"query":"mutation { sqlContextCreate(connectionId: \"postgres-jdbc-1a172a8859d-66a0bb96dac402dc\", defaultCatalog: \"demo\", defaultSchema: \"public\") { id connectionId defaultCatalog defaultSchema } }"}'
实测回显:
{
"data": {
"sqlContextCreate": {
"id": "postgres-jdbc-1a172a8859d-66a0bb96dac402dc:1",
"connectionId": "postgres-jdbc-1a172a8859d-66a0bb96dac402dc",
"defaultCatalog": "demo",
"defaultSchema": "public"
}
}
}
上下文 id 规则为 <连接id>:<序号>,同连接可开多个互相隔离的上下文;sqlContextDestroy 用于显式销毁。
5.2 提交异步执行 asyncSqlExecuteQuery
所有 SQL 都包装成异步任务,调用立即返回任务 id(会话内自增整数),不阻塞等待结果:
python3 gql.py 'mutation { asyncSqlExecuteQuery(connectionId: "postgres-jdbc-1a172a8859d-66a0bb96dac402dc", contextId: "postgres-jdbc-1a172a8859d-66a0bb96dac402dc:1", sql: "INSERT INTO sensor_log (device, value) VALUES ('"'"'sensor-a'"'"', 21.5), ('"'"'sensor-b'"'"', 19.8), ('"'"'sensor-a'"'"', 22.1)") { id running } }'
实测回显:
{
"data": {
"asyncSqlExecuteQuery": {
"id": "8",
"running": true
}
}
}
5.3 轮询任务 asyncTaskInfo
python3 gql.py 'mutation { asyncTaskInfo(id: "8", removeOnFinish: false) { id running status error { message } } }'
实测回显:
{
"data": {
"asyncTaskInfo": {
"id": "8",
"running": false,
"status": "Finished",
"error": null
}
}
}
removeOnFinish: false 表示查询后保留任务结果,供下一步取数;任务失败时 error.message 带 SQLState(见 5.5 节排障实录)。
5.4 取回结果 asyncSqlExecuteResults
任务结束后取执行信息。DML 看 updateRowCount:
python3 gql.py 'mutation { asyncSqlExecuteResults(taskId: "8") { statusMessage duration results { updateRowCount dataFormat sourceQuery } } }'
实测回显(3 行插入,耗时 2ms):
{
"data": {
"asyncSqlExecuteResults": {
"statusMessage": "Executed",
"duration": 2,
"results": [
{
"updateRowCount": 3.0,
"dataFormat": "resultset",
"sourceQuery": null
}
]
}
}
}
SELECT 则取 resultSet 里的列元数据与行数据。先提交查询任务(返回 id: "9"),再取结果:
python3 gql.py 'mutation { asyncSqlExecuteQuery(connectionId: "postgres-jdbc-1a172a8859d-66a0bb96dac402dc", contextId: "postgres-jdbc-1a172a8859d-66a0bb96dac402dc:1", sql: "SELECT id, device, value, logged_at FROM sensor_log ORDER BY id") { id running } }'
python3 gql.py 'mutation { asyncSqlExecuteResults(taskId: "9") { statusMessage duration results { updateRowCount dataFormat sourceQuery resultSet { id columns { label name dataKind typeName } rowsWithMetaData { data } } } } }'
实测回显(列类型带 PG 内部类型名,行数据全部按字符串序列化):
{
"data": {
"asyncSqlExecuteResults": {
"statusMessage": "Executed",
"duration": 46,
"results": [
{
"updateRowCount": null,
"dataFormat": "resultset",
"sourceQuery": null,
"resultSet": {
"id": "1",
"columns": [
{
"label": "id",
"name": "id",
"dataKind": "NUMERIC",
"typeName": "serial4"
},
{
"label": "device",
"name": "device",
"dataKind": "STRING",
"typeName": "text"
},
{
"label": "value",
"name": "value",
"dataKind": "NUMERIC",
"typeName": "numeric"
},
{
"label": "logged_at",
"name": "logged_at",
"dataKind": "DATETIME",
"typeName": "timestamptz"
}
],
"rowsWithMetaData": [
{
"data": [
"1",
"sensor-a",
"21.5",
"2026-10-25 19:29:37.858 +0800"
]
},
{
"data": [
"2",
"sensor-b",
"19.8",
"2026-10-25 19:29:37.858 +0800"
]
},
{
"data": [
"3",
"sensor-a",
"22.1",
"2026-10-25 19:29:37.858 +0800"
]
}
]
}
}
]
}
}
}
注意:行值统一序列化为字符串(21.5 而非数值),消费方要按 columns[].dataKind 自行转换。
5.5 排障实录:并发提交导致 DDL/DML 乱序
实测中曾把 DROP TABLE IF EXISTS sensor_log、CREATE TABLE sensor_log (...)、INSERT INTO ... 三条语句连续提交(任务 1/2/3),随后 INSERT 任务报错:
{
"data": {
"asyncTaskInfo": {
"id": "3",
"name": "SQL execute",
"running": false,
"status": "Process query INSERT INTO sensor_log (device, value) VALUES ('sensor-a', 21.5), ('sensor-b', 19.8), ('sensor-a', 22.1)",
"error": {
"message": "Error executing query:\nSQL Error [42P01]: ERROR: relation \"sensor_log\" does not exist\n Position: 13"
},
"taskResult": null
}
}
}
但任务 1、2 都显示 status: "Finished"、error: null。直接在库里核查发现表不存在——三条异步任务并发执行,DROP 实际晚于 CREATE 完成,把刚建的表删掉了。改为「提交一条 → 轮询 asyncTaskInfo 确认 running: false 且无 error → 再提交下一条」后,CREATE 落库成功、INSERT 返回 updateRowCount: 3.0。结论:多语句之间有依赖时必须串行轮询,或合并为一个脚本一次提交,不能依赖提交顺序。
六、元数据与 DDL 域
6.1 导航树下钻 navNodeChildren
数据库对象组织成一棵导航树,节点用 URI 定位,格式为 node://g_GlobalConfiguration/datasources/<连接id>/<层级路径>。从连接根开始逐级下钻(每次取 name nodeType uri hasChildren):
python3 gql.py 'query { navNodeChildren(parentPath: "node://g_GlobalConfiguration/datasources") { name nodeType uri hasChildren folder } }'
实测回显(连接层):
{
"data": {
"navNodeChildren": [
{
"name": "maria-demo",
"nodeType": "Connection",
"uri": "node://g_GlobalConfiguration/datasources/mariaDB-1a172ab0e54-d5008b42ec7a307",
"hasChildren": true,
"folder": false
},
{
"name": "pg-demo",
"nodeType": "Connection",
"uri": "node://g_GlobalConfiguration/datasources/postgres-jdbc-1a172a8859d-66a0bb96dac402dc",
"hasChildren": true,
"folder": false
}
]
}
}
逐级下钻 pg-demo 依次得到 database(Databases)→ database/demo_16384(库节点,URI 带 PG OID 后缀)→ .../schema → schema/public_2200 → .../table,最终列出表节点:
{
"data": {
"navNodeChildren": [
{
"name": "sensor_log",
"nodeType": "Table",
"uri": "node://g_GlobalConfiguration/datasources/postgres-jdbc-1a172a8859d-66a0bb96dac402dc/database/demo_16384/schema/public_2200/table/sensor_log_16400",
"hasChildren": true
}
]
}
}
URI 必须由上级查询返回,不能凭名字拼接——对象节点带 OID 后缀(demo_16384、public_2200、sensor_log_16400),凭猜测拼出的 URI 会报 Node '...' not found(io.cloudbeaver.DBWebException)。
6.2 获取对象 DDL metadataGetNodeDDL
拿上一步的表节点 URI 取 DDL(参数名是 nodeId,不是 nodePath):
python3 gql.py 'query { metadataGetNodeDDL(nodeId: "node://g_GlobalConfiguration/datasources/postgres-jdbc-1a172a8859d-66a0bb96dac402dc/database/demo_16384/schema/public_2200/table/sensor_log_16400") }'
实测回显:
{
"data": {
"metadataGetNodeDDL": "-- Drop table\n\n-- DROP TABLE public.sensor_log;\n\nCREATE TABLE public.sensor_log (\n\tid serial4 NOT NULL,\n\tdevice text NOT NULL,\n\tvalue numeric(8, 2) NULL,\n\tlogged_at timestamptz DEFAULT now() NULL,\n\tCONSTRAINT sensor_log_pkey PRIMARY KEY (id)\n);\n"
}
}
七、数据导出域
7.1 可用导出格式
python3 gql.py 'query { dataTransferAvailableStreamProcessors { id name fileExtension } }'
实测回显:
{
"data": {
"dataTransferAvailableStreamProcessors": [
{
"id": "stream_consumer:stream.csv",
"name": "CSV",
"fileExtension": "csv"
},
{
"id": "stream_consumer:stream.dbunit",
"name": "DbUnit",
"fileExtension": "xml"
},
{
"id": "stream_consumer:stream.html",
"name": "HTML",
"fileExtension": "html"
},
{
"id": "stream_consumer:stream.json",
"name": "JSON",
"fileExtension": "json"
},
{
"id": "stream_consumer:stream.markdown.table",
"name": "Markdown",
"fileExtension": "md"
},
{
"id": "stream_consumer:stream.sql",
"name": "SQL",
"fileExtension": "sql"
},
{
"id": "stream_consumer:stream.source.code",
"name": "Source code",
"fileExtension": "php"
},
{
"id": "stream_consumer:stream.txt",
"name": "TXT",
"fileExtension": "txt"
},
{
"id": "stream_consumer:stream.xml",
"name": "XML",
"fileExtension": "xml"
},
{
"id": "stream_consumer:stream.xlsx",
"name": "XLSX",
"fileExtension": "xlsx"
}
]
}
}
7.2 提交导出任务
dataTransferExportDataFromContainer 直接对表节点导出(processorProperties 是必填,给空对象即用默认 CSV 参数):
python3 gql.py 'query { dataTransferExportDataFromContainer(connectionId: "postgres-jdbc-1a172a8859d-66a0bb96dac402dc", containerNodePath: "node://g_GlobalConfiguration/datasources/postgres-jdbc-1a172a8859d-66a0bb96dac402dc/database/demo_16384/schema/public_2200/table/sensor_log_16400", parameters: { processorId: "stream_consumer:stream.csv", processorProperties: {} }) { id running status } }'
实测回显(小表同步就完成了):
{
"data": {
"dataTransferExportDataFromContainer": {
"id": "10",
"running": false,
"status": "Finished"
}
}
}
从任务结果里取文件名:
python3 gql.py 'mutation { asyncTaskInfo(id: "10", removeOnFinish: false) { id running status taskResult error { message } } }'
实测回显:
{
"data": {
"asyncTaskInfo": {
"id": "10",
"running": false,
"status": "Finished",
"taskResult": "l0xl9dtgmgzy12afx12noym458_424a49e4-239c-4452-8d44-e41bde5460a1.csv",
"error": null
}
}
}
7.3 下载导出文件
导出文件经普通 HTTP GET 下载,路径为 /api/data/<taskResult 文件名>,需带同一会话 Cookie:
curl -s -b ck.txt -D - http://127.0.0.1:8978/api/data/l0xl9dtgmgzy12afx12noym458_424a49e4-239c-4452-8d44-e41bde5460a1.csv -o sensor_log.csv
实测响应头与文件内容:
HTTP/1.1 200 OK
Date: Sun, 25 Oct 2026 11:31:40 GMT
Content-Type: text/plain;charset=iso-8859-1
Content-Disposition: attachment; filename="sensor_log.csv"
Transfer-Encoding: chunked
"id","device","value","logged_at"
1,sensor-a,21.50,2026-10-25 19:29:37.858 +0800
2,sensor-b,19.80,2026-10-25 19:29:37.858 +0800
3,sensor-a,22.10,2026-10-25 19:29:37.858 +0800
八、错误格式与排障速查
8.1 三类错误形态
实测遇到的错误统一经 GraphQL errors 数组返回,按 extensions.classification 分三类:
| 分类 | 触发场景 | 实测特征 |
|---|---|---|
ValidationError | 字段名/参数写错(如把管理操作当 mutation、字段名拼错) | message 以 Validation error (FieldUndefined...) 开头,HTTP 200 |
DataFetchingException | 业务/数据库错误(口令错误、SQL 报错、节点不存在) | extensions 带 exceptionClass(如 PSQLException)、databaseState(SQLState)、完整 Java 堆栈 |
BadFaithIntrospection | 单请求内省字段过多 | 见 2.4 节 |
注意:业务错误时 HTTP 状态码仍是 200,必须检查响应体里的 errors,不能只判断传输层状态。
8.2 权限与会话错误码
webErrorCode 扩展字段用于程序化处理。未登录调用管理接口 listUsers 实测:
{
"errors": [
{
"message": "Access denied",
"locations": [
{
"line": 1,
"column": 9
}
],
"path": [
"listUsers"
],
"extensions": {
"exceptionClass": "io.cloudbeaver.DBWebExceptionAccessDenied",
"stackTrace": "io.cloudbeaver.DBWebExceptionAccessDenied: Access denied",
"webErrorCode": "accessDenied",
"classification": "DataFetchingException"
}
}
],
"data": null
}
会话过期(cb-session-id 失效后)再调用的实测:
{
"errors": [
{
"message": "Session has expired",
"locations": [
{
"line": 1,
"column": 9
}
],
"path": [
"sessionState"
],
"extensions": {
"exceptionClass": "io.cloudbeaver.DBWebException",
"stackTrace": "io.cloudbeaver.DBWebException: Session has expired\n\tat io.cloudbeaver.service.session.CBSessionManager.findWebSession(CBSessionManager.java:344)",
"webErrorCode": "sessionExpired",
"classification": "DataFetchingException"
}
}
],
"data": null
}
消费方收到 sessionExpired 应重新 authLogin;会话空闲超时由服务端 expireSessionAfterPeriod 控制(默认 1800000ms,改法见《CloudBeaver 26.2.3 配置参数详解》)。
8.3 排障速查表
| 现象 | 原因 | 处理 |
|---|---|---|
Field 'authLogout' in type 'Mutation' is undefined | 把 Query 当 Mutation 用(CloudBeaver 大量操作是 Query,且登出叫 authLogoutExtended) | 换用 query { ... },或先内省确认 |
BadFaithIntrospection | 单次内省请求字段过多 | 拆成多次小请求 |
Node '...' not found | 导航 URI 凭名字拼接 | 逐级 navNodeChildren 取真实 URI(带 OID 后缀) |
异步任务 error 带 SQL Error [42P01] | 多条 SQL 并发提交导致乱序 | 串行提交并轮询 asyncTaskInfo,见 5.5 节 |
asyncSqlExecuteResults 返回 null 值错误 | 任务结果已被取走或任务失败无结果集 | 先查 asyncTaskInfo 确认成功;结果只取一次 |
Missing field argument 'processorProperties' | 导出参数缺必填子字段 | 传 processorProperties: {} 用默认 |
九、实战封装建议
把 2.3 节的 gql.py 扩成三行一调的执行器即可覆盖 90% 场景:exec_sql(sql) 内部按「asyncSqlExecuteQuery 提交 → 循环 asyncTaskInfo 至 running: false → asyncSqlExecuteResults 取回」串行执行,正好规避 5.5 节的并发乱序问题。集成到运维脚本时注意三点:行值全是字符串需按 dataKind 转型;errors 数组必须先于 data 检查;长会话脚本要在捕获 sessionExpired 后自动重登。
十、参考链接
- CloudBeaver 官方 Wiki:Server Architecture(查询日期:2026-10-25,实时事件走 WebSocket、阻断时回退 HTTP 长轮询)
- CloudBeaver 官方 Wiki:Server configuration(查询日期:2026-10-25)
- CloudBeaver GitHub 仓库(源码与 Schema)(查询日期:2026-10-25)
- 本站前文:《CloudBeaver 26.2.3 Docker Compose 部署指南》、《CloudBeaver 26.2.3 配置参数详解》、《CloudBeaver 26.2.3 介绍与选型指南》