| name | hai-usage-reldb |
| description | Use when: using @h-ai/reldb, database operations, SQL queries, DDL, CRUD, transactions, pagination, JSON operations, BaseReldbCrudRepository, HaiReldbError, table creation, database access. 使用 @h-ai/reldb 进行 SQLite/PostgreSQL/MySQL 的初始化、SQL/DDL/CRUD/事务与分页操作。 |
hai-usage-reldb — 数据库操作指南
能力契约
| 项目 | 契约 |
|---|
| 能力 | Use when: using @h-ai/reldb, database operations, SQL queries, DDL, CRUD, transactions, pagination, JSON operations, BaseReldbCrudRepository, HaiReldbError, table creation, database access. 使用 @h-ai/reldb 进行 SQLite/PostgreSQL/MySQL 的初始化、SQL/DDL/CRUD/事务与分页操作。 |
| 适用场景 | 当任务与 hai-usage-reldb 的能力描述匹配,并且需要遵循本 Skill 的流程和边界时 |
| 输入 | 模块配置、类型化业务参数、依赖初始化状态和目标运行环境 |
| 输出 | 符合模块公共 API 的实现或示例;业务结果使用 HaiResult,并同步必要测试与文档 |
| 限制 | 遵守 init → use → close 生命周期与运行环境边界;不绕过类型、授权、输入校验或敏感信息保护 |
@h-ai/reldb 提供统一的数据库操作接口,支持 SQLite、PostgreSQL、MySQL,包含 DDL、SQL、CRUD 抽象、事务与分页。
§0 如何使用本文档
~360 行,按任务主题只读对应小节(初始化 / SQL / DDL / CRUD / 事务 / 分页),不需整文加载。
§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。
初始化与关闭
import { core } from '@h-ai/core'
import { reldb } from '@h-ai/reldb'
await reldb.init(core.config.get('db'))
await reldb.close()
命名约定(与模块规范保持一致)
- 表名必须使用
hai_<module>_<feature>(如 hai_iam_users)
- 缓存 key(如该仓库逻辑涉及 cache)必须使用
hai:<module>:<feature>(如 hai:iam:user:123)
- 表名/缓存 key 常量应在使用处就近定义,不做配置项暴露
§2 操作接口总览
| 接口 | 用途 | 入口 |
|---|
| DDL | 建表/索引/字段变更 | reldb.ddl |
| SQL | 原始查询/执行/分页 | reldb.sql |
| CRUD | 通用增删改查 | reldb.crud.table(config) |
| 仓库 | 业务数据仓库封装 | extends BaseReldbCrudRepository |
| 事务 | 事务管理 | reldb.tx |
| 分页 | 分页参数与结果 | reldb.pagination |
| JSON | JSON 路径操作 | reldb.json |
§3 DDL — reldb.ddl
| 方法 | 签名 | 说明 |
|---|
createTable | (tableName, columns: TableDef, ifNotExists?) => HaiResult<void> | 建表(默认 IF NOT EXISTS) |
dropTable | (tableName, ifExists?) => HaiResult<void> | 删除表 |
addColumn | (tableName, columnName, columnDef) => HaiResult<void> | 添加列 |
dropColumn | (tableName, columnName) => HaiResult<void> | 删除列 |
renameTable | (oldName, newName) => HaiResult<void> | 重命名表 |
createIndex | (tableName, indexName, indexDef) => HaiResult<void> | 创建索引 |
dropIndex | (indexName, ifExists?) => HaiResult<void> | 删除索引 |
raw | (sql: string) => HaiResult<void> | 执行原始 DDL |
所有方法返回 Promise<HaiResult<void>>。
TableDef 与 ColumnDef
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' },
}
跨数据库类型映射
| 类型 | 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 | TIMESTAMPTZ | DATETIME |
| JSON | TEXT | JSONB | JSON |
§4 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) => HaiResult<PaginatedResult> | 分页查询 |
参数占位符统一使用 ?(PostgreSQL 的 $n 由 Provider 自动转换):
const result = await reldb.sql.query<User>(
'SELECT * FROM hai_demo_users WHERE status = ? AND age > ?',
['active', 18],
)
分页查询:
const page = await reldb.sql.queryPage<User>({
sql: 'SELECT * FROM hai_demo_users WHERE status = ? ORDER BY id',
params: ['active'],
pagination: { page: 1, pageSize: 20 },
overrides: { maxPageSize: 100 },
})
§5 CRUD — reldb.crud.table(config)
通过配置创建单表 CRUD 仓库,免写 SQL:
const userCrud = reldb.crud.table<{ id: number, name: string, email: string }>({
table: 'hai_demo_users',
idColumn: 'id',
select: ['id', 'name', 'email'],
createColumns: ['name', 'email'],
updateColumns: ['name', 'email'],
dbType: 'sqlite',
})
CRUD 方法
| 方法 | 签名 | 说明 |
|---|
create | (data, tx?) => HaiResult<ExecuteResult> | 创建 |
createMany | (items, tx?) => HaiResult<void> | 批量创建 |
createOrUpdate | (data, tx?) => HaiResult<ExecuteResult> | 创建或更新(upsert) |
findById | (id, tx?) => HaiResult<T | null> | 按 ID 查(不存在返回 null) |
getById | (id, tx?) => HaiResult<T> | 按 ID 获取(不存在返回 RECORD_NOT_FOUND 错误) |
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 事务参数。
使用示例
await userCrud.create({ name: '张三', email: 'test@example.com' })
await userCrud.createOrUpdate({ id: 1, name: '张三', email: 'new@example.com' })
const actives = await userCrud.findAll({
where: 'name LIKE ?',
params: ['%张%'],
orderBy: 'id DESC',
limit: 10,
})
const page = await userCrud.findPage({
where: 'name LIKE ?',
params: ['%张%'],
pagination: { page: 1, pageSize: 20 },
})
await reldb.tx.wrap(async (tx) => {
await userCrud.create({ name: '用户A', email: 'a@test.com' }, tx)
await userCrud.updateById(1, { name: '新名字' }, tx)
})
§6 BaseReldbCrudRepository
业务仓库基类,提供字段映射、自动建表与类型转换:
import { BaseReldbCrudRepository } from '@h-ai/reldb'
interface User { id: number, name: string, email: string, createdAt: Date }
class UserRepository extends BaseReldbCrudRepository<User> {
constructor() {
super(db, {
table: 'hai_demo_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 },
],
})
}
async findByEmail(email: string, tx?: DmlWithTxOperations) {
return this.sql(tx).get<User>('SELECT * FROM hai_demo_users WHERE email = ?', [email])
}
}
this.sql(tx?):返回当前数据操作对象(自动适配事务)。
自动能力:createdAt/updatedAt 自动时间戳、BOOLEAN → 1/0、TIMESTAMP → 毫秒、JSON → 序列化。
createOrUpdate:repo.createOrUpdate({ id: 1, name: '新名字' }) — 主键存在时更新(保留 createdAt),否则插入。dbType 自动获取,无需手动传入。
§7 事务 — reldb.tx
const result = await reldb.tx.wrap(async (tx) => {
await tx.execute('INSERT INTO hai_demo_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 hai_demo_users (name) VALUES (?)', ['李四'])
await tx.commit()
} catch {
await tx.rollback()
}
}
事务内可用:tx.query / tx.get / tx.execute / tx.batch / tx.queryPage / tx.crud.table / tx.commit / tx.rollback
§8 JSON 操作 — reldb.json
跨数据库统一的 JSON 路径操作(路径以 $ 开头):
const { sql, params } = reldb.json.extract('settings', '$.theme')
const rows = await reldb.sql.query(`SELECT * FROM hai_demo_users WHERE ${sql} = ?`, [...params, '"dark"'])
const { sql, params } = reldb.json.set('settings', '$.theme', 'dark')
await reldb.sql.execute(`UPDATE hai_demo_users SET settings = ${sql} WHERE id = ?`, [...params, userId])
const { sql, params } = reldb.json.remove('settings', '$.deprecated')
await reldb.sql.execute(`UPDATE hai_demo_users SET settings = ${sql} WHERE id = ?`, [...params, id])
const { sql, params } = reldb.json.merge('profile', { bio: '新简介', avatar: null })
await reldb.sql.execute(`UPDATE hai_demo_users SET profile = ${sql} WHERE id = ?`, [...params, userId])
column 参数为列名,禁止传入用户输入。
§9 分页 — reldb.pagination
| 方法 | 签名 | 说明 |
|---|
normalize | (options?, overrides?) => NormalizedPagination | 规范化分页参数 |
build | <T>(items, total, pagination) => PaginatedResult<T> | 构建分页结果 |
默认值:page=1, pageSize=20, maxPageSize=200。
§10 错误码 — 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 失败 |
§11 常见模式
API 端点中使用
import { kit } from '@h-ai/kit'
export const GET = kit.handler(async ({ url }) => {
const pagination = kit.validate.query(url, PaginationQuerySchema)
const result = await userCrud.findPage({ pagination, 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
})
SQL 安全
- 所有 SQL 必须参数化(
? 占位符),禁止字符串拼接/模板字面量
reldb.json 的 column 参数禁止传入用户输入
示例触发语句
- "创建数据库表"
- "写一个分页查询"
- "使用事务"
- "创建 Repository"
- "JSON 列操作"