| name | hai-reldb |
| description | 使用 @h-ai/reldb 进行 SQLite/PostgreSQL/MySQL 的初始化、SQL/DDL/CRUD/事务与分页操作;当需求涉及数据库访问、CRUD 仓库、事务处理、分页查询或 HaiReldbError 分支处理时使用。 |
hai-reldb
能力契约
| 项目 | 契约 |
|---|
| 能力 | 使用 @h-ai/reldb 进行 SQLite/PostgreSQL/MySQL 的初始化、SQL/DDL/CRUD/事务与分页操作;当需求涉及数据库访问、CRUD 仓库、事务处理、分页查询或 HaiReldbError 分支处理时使用。 |
| 适用场景 | 当任务与 hai-reldb 的能力描述匹配,并且需要遵循本 Skill 的流程和边界时 |
| 输入 | 模块配置、类型化业务参数、依赖初始化状态和目标运行环境 |
| 输出 | 符合模块公共 API 的实现或示例;业务结果使用 HaiResult,并同步必要测试与文档 |
| 限制 | 遵守 init → use → close 生命周期与运行环境边界;不绕过类型、授权、输入校验或敏感信息保护 |
@h-ai/reldb 提供统一的数据库操作接口,支持 SQLite、PostgreSQL、MySQL,包含 DDL、SQL、CRUD 抽象、事务与分页。
运行环境
⚠️ 服务端模块(Node.js only)。 浏览器端请通过 @h-ai/api-contract + @h-ai/serv 暴露的 API 端点间接访问数据库。
适用场景
- 新增或修改数据库访问逻辑(SQL/DDL/CRUD/事务)
- 使用
reldb.crud.table 或 BaseReldbCrudRepository 构建数据仓库
- 处理分页查询与分页结果规范化
- 基于
HaiReldbError 做错误分支处理
- JSON 列的路径提取、设置、插入、删除、合并操作(跨数据库统一语法)
使用步骤
1. 配置
type: ${HAI_RELDB_TYPE:sqlite}
database: ${HAI_RELDB_DATABASE:./data/app.db}
operationLog:
read: false
write: false
maxLength: 1000
level: debug
operationLog 在 reldb provider-base 的 DML 操作层统一输出日志,不要在 reldb-main.ts、各 raw provider 或调用方重复包装。read 覆盖 query/get/queryPage,write 覆盖 DDL、execute/batch 与 tx.begin/commit/rollback,maxLength 用于截断序列化后的 SQL 参数和批量语句,level 默认 debug。
2. 初始化与关闭
import { core } from '@h-ai/core'
import { reldb } from '@h-ai/reldb'
await reldb.init(core.config.get('db'))
await reldb.close()
reldb.config 返回的是脱敏后的配置快照;数据库密码与 URL 内嵌凭证会自动隐藏。
3. 选择操作接口
| 接口 | 用途 | 入口 |
|---|
| DDL | 建表/索引/字段变更 | reldb.ddl |
| SQL | 原始查询/执行/分页 | reldb.sql |
| CRUD | 通用增删改查 | reldb.crud.table(config) |
| 仓库 | 业务数据仓库封装 | extends BaseReldbCrudRepository |
| 事务 | 事务管理 | reldb.tx |
| 分页 | 分页参数与结果 | reldb.pagination |
| JSON | JSON 路径操作 | reldb.json |
核心 API
DDL — reldb.ddl
| 方法 | 签名 | 说明 |
|---|
createTable | (tableName, columns: TableDef, ifNotExists?: boolean) => HaiResult<void> | 建表(默认 IF NOT EXISTS) |
dropTable | (tableName, ifExists?: boolean) => HaiResult<void> | 删除表(默认 IF EXISTS) |
addColumn | (tableName, columnName, columnDef: ColumnDef) => HaiResult<void> | 添加列 |
dropColumn | (tableName, columnName) => HaiResult<void> | 删除列 |
renameTable | (oldName, newName) => HaiResult<void> | 重命名表 |
createIndex | (tableName, indexName, indexDef: IndexDef) => HaiResult<void> | 创建索引 |
dropIndex | (indexName, ifExists?: boolean) => HaiResult<void> | 删除索引 |
raw | (sql: string) => HaiResult<void> | 执行原始 DDL |
所有方法均返回 Promise<HaiResult<void>>,上表省略异步与错误类型。
TableDef(列名到列定义的映射):
const columns: TableDef = {
id: { type: 'INTEGER', primaryKey: true, autoIncrement: true },
name: { type: 'TEXT', notNull: true },
email: { type: 'TEXT', unique: true },
score: { type: 'REAL', defaultValue: 0 },
active: { type: 'BOOLEAN' },
metadata: { type: 'JSON' },
created_at: { type: 'TIMESTAMP' },
}
ColumnDef(列类型与约束):
interface ColumnDef {
type: 'TEXT' | 'INTEGER' | 'REAL' | 'BLOB' | 'BOOLEAN' | 'TIMESTAMP' | 'JSON'
primaryKey?: boolean
autoIncrement?: boolean
notNull?: boolean
unique?: boolean
defaultValue?: string | number | boolean | null
references?: { table: string, column: string, onDelete?: 'CASCADE' | 'SET NULL' | 'RESTRICT' | 'NO ACTION', onUpdate?: '...' }
}
跨数据库类型映射:
| 类型 | SQLite | PostgreSQL | MySQL |
|---|
| TEXT | TEXT | TEXT | VARCHAR(255) |
| INTEGER | INTEGER | INTEGER/SERIAL | INT/BIGINT |
| REAL | REAL | DOUBLE PRECISION | DOUBLE |
| BLOB | BLOB | BYTEA | BLOB |
| BOOLEAN | INTEGER | BOOLEAN | TINYINT(1) |
| TIMESTAMP | INTEGER | TIMESTAMP | DATETIME |
| JSON | TEXT | JSONB | JSON |
MySQL 将 TEXT 映射为 VARCHAR(255) 以支持索引和 UNIQUE 约束。INTEGER + autoIncrement 在 MySQL 映射为 BIGINT。
SQL — reldb.sql
| 方法 | 签名 | 说明 |
|---|
query | <T>(sql, params?) => HaiResult<T[]> | 查询返回多行 |
get | <T>(sql, params?) => HaiResult<T | null> | 查询返回单行 |
execute | (sql, params?) => HaiResult<ExecuteResult> | 执行写操作 |
batch | (statements: Array<{ sql, params? }>) => HaiResult<void> | 批量执行 |
queryPage | <T>(options: PaginationQueryOptions) => HaiResult<PaginatedResult> | 分页查询 |
所有方法均返回 Promise<HaiResult<T>>,上表省略异步与错误类型。
参数占位符统一使用 ?(PostgreSQL 的 $n 由 Provider 自动转换):
const result = await reldb.sql.query<User>(
'SELECT * FROM users WHERE status = ? AND age > ?',
['active', 18],
)
ExecuteResult:
interface ExecuteResult {
changes: number
lastInsertRowid?: number | bigint
}
分页查询:
const page = await reldb.sql.queryPage<User>({
sql: 'SELECT * FROM users WHERE status = ? ORDER BY id',
params: ['active'],
pagination: { page: 1, pageSize: 20 },
overrides: { maxPageSize: 100 },
})
CRUD — reldb.crud.table(config)
通过配置创建单表 CRUD 仓库,免写 SQL:
const userCrud = reldb.crud.table<{ id: number, name: string, email: string }>({
table: 'users',
idColumn: 'id',
select: ['id', 'name', 'email'],
createColumns: ['name', 'email'],
updateColumns: ['name', 'email'],
})
返回的 ReldbCrudRepository 方法:
| 方法 | 签名 | 说明 |
|---|
create | (data, tx?) => HaiResult<ExecuteResult> | 创建单条 |
createMany | (items, tx?) => HaiResult<void> | 批量创建 |
findById | (id, tx?) => HaiResult<T | null> | 按 ID 查询 |
findAll | (options?, tx?) => HaiResult<T[]> | 条件查询 |
findPage | (options, tx?) => HaiResult<PaginatedResult<T>> | 分页查询 |
updateById | (id, data, tx?) => HaiResult<ExecuteResult> | 按 ID 更新 |
deleteById | (id, tx?) => HaiResult<ExecuteResult> | 按 ID 删除 |
count | (options?, tx?) => HaiResult<number> | 计数 |
exists | (options?, tx?) => HaiResult<boolean> | 条件是否存在 |
existsById | (id, tx?) => HaiResult<boolean> | ID 是否存在 |
所有方法均支持可选 tx 事务参数。create/updateById 中的 data 会根据 createColumns/updateColumns 白名单过滤列。
使用示例:
const result = await userCrud.create({ name: '张三', email: 'test@example.com' })
await userCrud.createMany([
{ name: '用户A', email: 'a@test.com' },
{ name: '用户B', email: 'b@test.com' },
])
const user = await userCrud.findById(1)
const actives = await userCrud.findAll({
where: 'name LIKE ?',
params: ['%张%'],
orderBy: 'id DESC',
limit: 10,
offset: 0,
})
const page = await userCrud.findPage({
where: 'name LIKE ?',
params: ['%张%'],
orderBy: 'id DESC',
pagination: { page: 1, pageSize: 20 },
})
await userCrud.updateById(1, { name: '新名字' })
await userCrud.deleteById(1)
const total = await userCrud.count({ where: 'name LIKE ?', params: ['%张%'] })
const has = await userCrud.exists({ where: 'email = ?', params: ['test@example.com'] })
const found = await userCrud.existsById(1)
await reldb.tx.wrap(async (tx) => {
await userCrud.create({ name: '用户A', email: 'a@test.com' }, tx)
await userCrud.updateById(1, { name: '新名字' }, tx)
const user = await userCrud.findById(1, tx)
})
BaseReldbCrudRepository
业务仓库基类,提供字段映射、自动建表与类型转换能力:
import { BaseReldbCrudRepository } from '@h-ai/reldb'
interface User { id: number, name: string, email: string, createdAt: Date, updatedAt: Date }
class UserRepository extends BaseReldbCrudRepository<User> {
constructor() {
super(db, {
table: 'users',
idColumn: 'id',
fields: [
{ fieldName: 'id', columnName: 'id', def: { type: 'INTEGER', primaryKey: true, autoIncrement: true }, select: true, create: false, update: false },
{ fieldName: 'name', columnName: 'name', def: { type: 'TEXT', notNull: true }, select: true, create: true, update: true },
{ fieldName: 'email', columnName: 'email', def: { type: 'TEXT', notNull: true }, select: true, create: true, update: true },
{ fieldName: 'createdAt', columnName: 'created_at', def: { type: 'TIMESTAMP', notNull: true }, select: true, create: true, update: false },
{ fieldName: 'updatedAt', columnName: 'updated_at', def: { type: 'TIMESTAMP', notNull: true }, select: true, create: true, update: false },
],
})
}
async findByEmail(email: string, tx?: DmlWithTxOperations) {
return this.sql(tx).get<User>('SELECT * FROM users WHERE email = ?', [email])
}
}
this.sql(tx?):返回 DmlOperations(reldb.sql 或传入的事务句柄),自动适配事务场景。
自动能力:
createdAt / updatedAt 字段自动填充时间戳
- 主键生成(非 autoIncrement 主键默认使用
crypto.randomUUID())
- BOOLEAN → 1/0(SQLite/MySQL)或 true/false(PostgreSQL)
- TIMESTAMP → 毫秒时间戳(SQLite)或 Date(PG/MySQL)
- JSON → 字符串序列化(SQLite/MySQL)或原生 JSONB(PG)
事务 — reldb.tx
const result = await reldb.tx.wrap(async (tx) => {
await tx.execute('INSERT INTO users (name) VALUES (?)', ['张三'])
await tx.execute('INSERT INTO logs (action) VALUES (?)', ['user_created'])
return 'done'
})
const txResult = await reldb.tx.begin()
if (txResult.success) {
const tx = txResult.data
try {
await tx.execute('INSERT INTO users (name) VALUES (?)', ['李四'])
await tx.commit()
}
catch {
await tx.rollback()
}
}
事务内可用操作:
tx.query / tx.get / tx.execute / tx.batch / tx.queryPage
tx.crud.table(config) — 事务内的 CRUD 仓库
tx.commit() / tx.rollback()
分页 — reldb.pagination
| 方法 | 签名 | 说明 |
|---|
normalize | (options?, overrides?) => NormalizedPagination | 规范化分页参数(含 offset/limit 计算) |
build | <T>(items, total, pagination) => PaginatedResult<T> | 构建分页结果对象 |
interface PaginatedResult<T> {
items: T[]
total: number
page: number
pageSize: number
}
JSON — reldb.json
通过 reldb.json 构建跨数据库统一的 JSON 路径操作 SQL 表达式,返回 { sql, params } 可直接嵌入 reldb.sql.*。
路径格式遵循 SQL/JSON Path 标准,以 $ 开头(如 $.key、$.key.subkey、$[0])。
| 操作 | SQLite | PostgreSQL | MySQL |
|---|
extract | json_extract | #> (text[]) | JSON_EXTRACT |
set | json_set | jsonb_set | JSON_SET |
insert | json_insert | jsonb_insert | JSON_INSERT |
remove | json_remove | #- (text[]) | JSON_REMOVE |
merge | json_patch | ` | |
const { sql, params } = reldb.json.extract('settings', '$.theme')
const rows = await reldb.sql.query(
`SELECT * FROM users WHERE ${sql} = ?`,
[...params, '"dark"'],
)
const { sql, params } = reldb.json.set('settings', '$.theme', 'dark')
await reldb.sql.execute(
`UPDATE users SET settings = ${sql} WHERE id = ?`,
[...params, userId],
)
const { sql, params } = reldb.json.remove('settings', '$.deprecated')
await reldb.sql.execute(`UPDATE users SET settings = ${sql} WHERE id = ?`, [...params, id])
const { sql, params } = reldb.json.merge('profile', { bio: '新简介', avatar: null })
await reldb.sql.execute(`UPDATE users SET profile = ${sql} WHERE id = ?`, [...params, userId])
column 参数为列名,禁止传入用户输入(开发者负责安全性)。
错误码 — HaiReldbError
| 错误码 | code | 说明 |
|---|
HaiReldbError.CONNECTION_FAILED | hai:reldb:001 | 数据库连接失败 |
HaiReldbError.QUERY_FAILED | hai:reldb:002 | 查询或执行失败 |
HaiReldbError.CONSTRAINT_VIOLATION | hai:reldb:003 | 约束违反 |
HaiReldbError.TRANSACTION_FAILED | hai:reldb:004 | 事务失败 |
HaiReldbError.MIGRATION_FAILED | hai:reldb:005 | 迁移失败 |
HaiReldbError.RECORD_NOT_FOUND | hai:reldb:006 | 记录不存在 |
HaiReldbError.DUPLICATE_ENTRY | hai:reldb:007 | 重复条目 |
HaiReldbError.DEADLOCK | hai:reldb:008 | 死锁 |
HaiReldbError.TIMEOUT | hai:reldb:009 | 超时 |
HaiReldbError.NOT_INITIALIZED | hai:reldb:010 | 数据库未初始化 |
HaiReldbError.POOL_EXHAUSTED | hai:reldb:011 | 连接池耗尽 |
HaiReldbError.UNSUPPORTED_TYPE | hai:reldb:012 | 不支持的数据库类型 |
HaiReldbError.CONFIG_ERROR | hai:reldb:013 | 配置错误 |
HaiReldbError.DDL_FAILED | hai:reldb:020 | DDL 操作失败 |
常见模式
API 端点中使用
import { reldb, HaiReldbError } from '@h-ai/reldb'
import { kit } from '@h-ai/kit'
export async function GET(event) {
const { valid, data } = kit.validate.query(event.url, PageSchema)
if (!valid)
return kit.response.badRequest('Invalid params')
const result = await userCrud.findPage({
pagination: data,
orderBy: 'id ASC',
})
if (!result.success)
return kit.response.internalError()
return kit.response.ok(result.data)
}
事务跨仓库
const result = await reldb.tx.wrap(async (tx) => {
const user = await userRepo.create(userData, tx)
if (!user.success)
throw new Error(user.error.message)
await profileRepo.create({ userId: user.data.lastInsertRowid, ...profileData }, tx)
return user.data
})
BaseReldbCrudRepository 事务集成
const txResult = await reldb.tx.begin()
if (txResult.success) {
const tx = txResult.data
await userRepo.create({ name: '用户A', email: 'a@test.com' }, tx)
await userRepo.create({ name: '用户B', email: 'b@test.com' }, tx)
await tx.commit()
}
相关 Skills
hai-build:模块初始化顺序
hai-core:配置管理、HaiResult 模型
hai-iam:IAM 模块内部使用 reldb 进行用户/角色/权限存储
hai-cache:缓存穿透保护中配合 reldb 使用