都 2025 了,还在手搓 SQL 拼字符串?ArkTS 里把 RDB + ORM 一把梭不香吗?
·
我是兰瓶Coding,一枚刚踏入鸿蒙领域的转型小白,原是移动开发中级,如下是我学习笔记《零基础学鸿蒙》,若对你所有帮助,还请不吝啬的给个大大的赞~
前言
我见过太多“能跑但难维护”的数据层:业务里到处飞的 SQL、同一段 ResultSet 解析复制粘贴十遍、字段改名全项目红一片。别急着叹气,这篇就按你给的大纲来——RDB 架构 → SQL 执行 → 数据模型映射——把 RdbStore / ValuesBucket / ResultSet 一网打尽,再顺手落一个轻量 ORM 封装(真·能抄能用那种)。写完你能做到:DDL/CRUD/事务有章法,查询更优雅,数据模型一处维护全局受益。
一、RDB 架构:把“库、表、连接、版本、事务”摆整齐
核心角色速记
- RdbStore:数据库句柄(连接),你所有 SQL/CRUD 都从它发起。
- StoreConfig:数据库配置(文件名、安全级别等),带版本与升级回调。
- ValuesBucket:“一行数据”的键值容器,用于
insert/update。 - ResultSet:查询结果游标;自己迭代与 close,别泄漏句柄。
- RdbPredicates:条件构造器,给
query/update/delete传“WHERE/ORDER BY/LIMIT”。
推荐分层
/data
rdb/Database.ts // 获取 RdbStore + 版本升级
rdb/Tx.ts // 事务辅助
orm/Entity.ts // 元数据 + 映射工具
orm/Repository.ts // 通用 CRUD
repo/TodoRepo.ts // 业务仓库(组合查询)
二、起步:拿到 RdbStore,顺便把“升级”这事儿一次讲透
// data/rdb/Database.ts
import rdb from '@ohos.data.rdb'
export type Migrator = (db: rdb.RdbStore) => void
interface DbOptions {
name: string
version: number
onCreate: Migrator
onUpgrade?: (db: rdb.RdbStore, oldV: number, newV: number) => void
}
export class Database {
private store?: rdb.RdbStore
constructor(private opt: DbOptions) {}
async open(ctx: Context) {
const cfg: rdb.StoreConfig = { name: this.opt.name, securityLevel: rdb.SecurityLevel.S1 }
this.store = await rdb.getRdbStore(ctx, cfg, this.opt.version, (db, oldV) => {
if (oldV === 0) {
this.opt.onCreate(db)
} else if (this.opt.onUpgrade) {
this.opt.onUpgrade(db, oldV, this.opt.version)
}
})
return this.store!
}
get(): rdb.RdbStore {
if (!this.store) throw new Error('DB not opened')
return this.store
}
}
初始化示例(包含 DDL 与升级)
// data/Bootstrap.ts
import rdb from '@ohos.data.rdb'
import { Database } from './rdb/Database'
export const db = new Database({
name: 'app.db',
version: 2,
onCreate(db: rdb.RdbStore) {
db.executeSql(`
CREATE TABLE IF NOT EXISTS todos(
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
done INTEGER NOT NULL DEFAULT 0,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
);
`)
db.executeSql(`CREATE INDEX IF NOT EXISTS idx_todos_done ON todos(done);`)
},
onUpgrade(db: rdb.RdbStore, oldV: number, newV: number) {
if (oldV < 2) {
db.executeSql(`ALTER TABLE todos ADD COLUMN priority INTEGER NOT NULL DEFAULT 0;`)
db.executeSql(`CREATE INDEX IF NOT EXISTS idx_todos_priority ON todos(priority);`)
}
}
})
小贴士
- 版本升级回调里只做幂等 DDL(
IF NOT EXISTS),防止重复执行。- DDL 一定配索引(按查询条件上),别让搜索一跑就卡。
三、SQL 执行:从“裸 SQL”到“安全、可重复”的 CRUD
3.1 executeSql:DDL/原生 SQL、批处理、占位参数
const store = db.get()
// 占位参数用 ?,防注入
store.executeSql(
'UPDATE todos SET done = ?, updated_at = ? WHERE id = ?',
[1, Date.now(), 42]
)
3.2 insert/update/delete/query:配合 ValuesBucket/Predicates
import dataAbility from '@ohos.data.dataAbility'
import rdb from '@ohos.data.rdb'
// INSERT
function toValuesBucket(row: { [k: string]: any }): dataAbility.ValuesBucket {
const v = new dataAbility.ValuesBucket()
Object.keys(row).forEach(k => {
const val = row[k]
switch (typeof val) {
case 'string': v.putString(k, val); break
case 'number':
// 简单区分整型/浮点,可按你的表结构裁剪
Number.isInteger(val) ? v.putLong(k, val) : v.putDouble(k, val); break
case 'boolean': v.putLong(k, val ? 1 : 0); break
default: if (val == null) v.putNull(k); else v.putString(k, String(val))
}
})
return v
}
// 新建 todo
export async function addTodo(title: string) {
const now = Date.now()
const row = { title, done: 0, created_at: now, updated_at: now, priority: 0 }
const id = await store.insert('todos', toValuesBucket(row))
return id
}
// 查询 todo 列表(ResultSet → 数组)
export async function listOpenTodos(limit = 50, offset = 0) {
const pred = new rdb.RdbPredicates('todos')
.equalTo('done', 0)
.orderByAsc('priority')
.orderByDesc('updated_at')
.limitAs(limit)
.offsetAs(offset)
const rs = await store.query(pred, ['id', 'title', 'done', 'priority', 'updated_at'])
const rows: any[] = []
while (rs.goToNextRow()) {
rows.push({
id: rs.getLong(rs.getColumnIndex('id')),
title: rs.getString(rs.getColumnIndex('title')),
done: rs.getLong(rs.getColumnIndex('done')) === 1,
priority: rs.getLong(rs.getColumnIndex('priority')),
updatedAt: rs.getLong(rs.getColumnIndex('updated_at')),
})
}
rs.close()
return rows
}
// 更新(按主键)
export async function finishTodo(id: number) {
const v = toValuesBucket({ done: 1, updated_at: Date.now() })
const pred = new rdb.RdbPredicates('todos').equalTo('id', id)
const changed = await store.update(v, pred)
return changed
}
// 删除(批量条件)
export async function purgeDone(beforeMs: number) {
const pred = new rdb.RdbPredicates('todos')
.equalTo('done', 1)
.lessThan('updated_at', beforeMs)
return await store.delete(pred)
}
关键要领
- ValuesBucket 就像一个类型感知的 Map,记得按列类型选择
putLong/putDouble/putString/putNull。 - ResultSet 必须
close();列值取用getColumnIndex()→getXxx(),稳。 - Predicates 能覆盖大多数
WHERE:equalTo/like/between/in/and/or/orderBy/limit/offset。
3.3 事务:要么全成,要么全滚
export async function completeAll(ids: number[]) {
store.beginTransaction()
try {
for (const id of ids) {
const v = toValuesBucket({ done: 1, updated_at: Date.now() })
const pred = new rdb.RdbPredicates('todos').equalTo('id', id)
await store.update(v, pred)
}
store.commit()
} catch (e) {
store.rollBack()
throw e
}
}
事务内尽量只做本库写;有网络 / 跨源写入就分两阶段(或写出补偿日志)。
四、数据模型映射:给 SQL 穿件体面外套(轻量 ORM)
4.1 目标:定义一次表结构 + 字段映射,CRUD 自动带走
我们实现一套极轻的 ORM:
- EntityMeta:表名、主键、列定义、字段转换器;
- RowMapper:
ResultSet → 实体; - toValues:
实体 → ValuesBucket; - Repository:泛型 CRUD + 查询辅助。
(1) 元信息定义
// data/orm/Entity.ts
import dataAbility from '@ohos.data.dataAbility'
import rdb from '@ohos.data.rdb'
export type ColType = 'INTEGER' | 'REAL' | 'TEXT' | 'BLOB'
export interface ColumnMeta<T=any> {
name: string
type: ColType
fromDb?: (v: any) => T
toDb?: (v: T) => any
}
export interface EntityMeta<T> {
table: string
pk: keyof T
columns: Record<keyof T, ColumnMeta>
}
export function toValues<T>(meta: EntityMeta<T>, obj: Partial<T>): dataAbility.ValuesBucket {
const v = new dataAbility.ValuesBucket()
for (const k in obj) {
const c = meta.columns[k as keyof T]
if (!c) continue
const raw = (obj as any)[k]
const val = c.toDb ? c.toDb(raw) : raw
if (val === undefined) continue
switch (c.type) {
case 'INTEGER': v.putLong(c.name, Number(val)); break
case 'REAL': v.putDouble(c.name, Number(val)); break
case 'TEXT': v.putString(c.name, val == null ? null : String(val)); break
case 'BLOB': /* 自行实现二进制 */ break
}
}
return v
}
export function rowFromRs<T>(meta: EntityMeta<T>, rs: rdb.ResultSet): T {
const out: any = {}
for (const k in meta.columns) {
const c = meta.columns[k as keyof T]
const idx = rs.getColumnIndex(c.name)
let v: any = null
switch (c.type) {
case 'INTEGER': v = rs.getLong(idx); break
case 'REAL': v = rs.getDouble(idx); break
case 'TEXT': v = rs.getString(idx); break
case 'BLOB': /* 读取 blob */ break
}
out[k] = c.fromDb ? c.fromDb(v) : v
}
return out as T
}
(2) 针对 Todo 的实体与表
// data/entities/Todo.ts
export interface Todo {
id: number
title: string
done: boolean
priority: number
createdAt: number
updatedAt: number
}
import { EntityMeta } from '../orm/Entity'
export const TodoMeta: EntityMeta<Todo> = {
table: 'todos',
pk: 'id',
columns: {
id: { name: 'id', type: 'INTEGER' },
title: { name: 'title', type: 'TEXT' },
done: { name: 'done', type: 'INTEGER',
fromDb: (n) => n === 1, toDb: (b: boolean) => (b ? 1 : 0) },
priority: { name: 'priority', type: 'INTEGER' },
createdAt: { name: 'created_at', type: 'INTEGER' },
updatedAt: { name: 'updated_at', type: 'INTEGER' },
}
}
(3) 通用仓库(Repository)
// data/orm/Repository.ts
import rdb from '@ohos.data.rdb'
import { EntityMeta, toValues, rowFromRs } from './Entity'
export class Repository<T> {
constructor(private store: rdb.RdbStore, private meta: EntityMeta<T>) {}
async insert(partial: Omit<T, 'id'> & Partial<Pick<T, 'id'>>): Promise<number> {
const v = toValues(this.meta, partial)
return await this.store.insert(this.meta.table, v)
}
async updateById(id: number, partial: Partial<T>) {
const v = toValues(this.meta, partial)
const pred = new rdb.RdbPredicates(this.meta.table).equalTo(
this.meta.columns[this.meta.pk].name, id
)
return await this.store.update(v, pred)
}
async deleteById(id: number) {
const pred = new rdb.RdbPredicates(this.meta.table).equalTo(
this.meta.columns[this.meta.pk].name, id
)
return await this.store.delete(pred)
}
async findById(id: number): Promise<T | null> {
const pred = new rdb.RdbPredicates(this.meta.table).equalTo(
this.meta.columns[this.meta.pk].name, id
).limitAs(1)
const rs = await this.store.query(pred, Object.values(this.meta.columns).map(c => c.name))
const row = rs.goToNextRow() ? rowFromRs<T>(this.meta, rs) : null
rs.close()
return row
}
async list(where: (p: rdb.RdbPredicates) => rdb.RdbPredicates, limit = 50, offset = 0): Promise<T[]> {
let p = new rdb.RdbPredicates(this.meta.table)
p = where(p).limitAs(limit).offsetAs(offset)
const rs = await this.store.query(p, Object.values(this.meta.columns).map(c => c.name))
const out: T[] = []
while (rs.goToNextRow()) out.push(rowFromRs<T>(this.meta, rs))
rs.close()
return out
}
}
(4) 在业务里用它
// repo/TodoRepo.ts
import { Repository } from '../data/orm/Repository'
import { Todo, TodoMeta } from '../data/entities/Todo'
import { db } from '../data/Bootstrap'
import rdb from '@ohos.data.rdb'
export class TodoRepo {
private store!: rdb.RdbStore
private repo!: Repository<Todo>
async init(ctx: Context) {
this.store = await db.open(ctx)
this.repo = new Repository<Todo>(this.store, TodoMeta)
}
async add(title: string) {
const now = Date.now()
return await this.repo.insert({ title, done: false, priority: 0, createdAt: now, updatedAt: now } as any)
}
async markDone(id: number) {
return await this.repo.updateById(id, { done: true, updatedAt: Date.now() } as any)
}
async listOpen(keyword = '', limit = 100, offset = 0) {
return await this.repo.list(p => {
let q = p.equalTo('done', 0).orderByAsc('priority').orderByDesc('updated_at')
if (keyword) q = q.like('title', `%${keyword}%`)
return q
}, limit, offset)
}
}
现在你业务里看不到
ValuesBucket/ResultSet细节了,改表名/字段名只动实体元数据,映射自动跟上。舒服!
五、常用“姿势”合集:效率、正确性两手都要硬
5.1 分页 + 总数
export async function listWithCount(keyword = '') {
const pred = new rdb.RdbPredicates('todos')
if (keyword) pred.like('title', `%${keyword}%`)
const rs = await store.query(pred, ['COUNT(1) AS c'])
rs.goToNextRow()
const count = rs.getLong(rs.getColumnIndex('c'))
rs.close()
const list = await repo.list(p => (keyword ? p.like('title', `%${keyword}%`) : p).limitAs(20))
return { count, list }
}
5.2 批量写入:executeSql + 事务更快
store.beginTransaction()
try {
const sql = `INSERT INTO todos(title, done, priority, created_at, updated_at) VALUES(?, ?, ?, ?, ?)`
for (const t of batch) {
store.executeSql(sql, [t.title, 0, 0, t.ts, t.ts])
}
store.commit()
} catch (e) { store.rollBack(); throw e }
5.3 乐观并发控制(更新时带版本/时间戳)
export async function safeUpdate(id: number, lastUpdated: number, patch: any) {
const v = toValues(TodoMeta, { ...patch, updatedAt: Date.now() })
const pred = new rdb.RdbPredicates('todos')
.equalTo('id', id).equalTo('updated_at', lastUpdated)
const changed = await store.update(v, pred)
if (changed === 0) throw new Error('CONFLICT') // 有人抢先改过
return changed
}
5.4 “读写合一”的小封装(避免忘 close)
export async function queryOne<T>(sql: string, args: any[], map: (rs: rdb.ResultSet)=>T): Promise<T|null> {
const rs = await store.rawQuery(sql, args) // 若无 rawQuery,用 executeSql + 临时视图/或 predicates
const row = rs.goToNextRow() ? map(rs) : null
rs.close()
return row
}
六、把它塞回 ArkUI 页面里跑一圈(最小演示)
// pages/TodoPage.ets
import { TodoRepo } from '../repo/TodoRepo'
@Entry
@Component
struct TodoPage {
private repo: TodoRepo = new TodoRepo()
@State items: Array<{ id:number; title:string; done:boolean }> = []
@State title: string = ''
async aboutToAppear() {
await this.repo.init(getContext(this))
this.items = await this.repo.listOpen()
}
build() {
Column({ space: 12 }) {
Row({ space: 8 }) {
TextInput({ placeholder: 'Add a task…' }).onChange(v => this.title = v).layoutWeight(1)
Button('Add').onClick(async () => {
if (!this.title.trim()) return
await this.repo.add(this.title.trim())
this.items = await this.repo.listOpen()
this.title = ''
})
}.padding(12)
List() {
ForEach(this.items, (it) => ListItem() {
Row() {
Checkbox({ name: `cb-${it.id}`, group: 'todo', selected: it.done })
.onChange(async (_, checked) => {
await this.repo.markDone(it.id)
this.items = await this.repo.listOpen()
})
Text(it.title).opacity(it.done ? 0.5 : 1).decoration(it.done ? { type: TextDecorationType.LineThrough } : null)
}.padding(12)
}, it => String(it.id))
}
}.padding(12)
}
}
七、踩坑复盘(我都踩过,真诚分享)
- ResultSet 忘记 close → 句柄泄漏、奇怪卡顿:统一用小工具函数兜住。
- 类型不一致 →
putString/putLong乱用:给每列明确类型,在toDb/fromDb里转换好。 - 大表无索引 → 查询像“走路去隔壁城”:把WHERE/ORDER涉及列建索引。
- 事务里做网络 → 回滚/一致性都悬:事务只管本地写。
- 字段改名后大面积爆红 → 抽象成实体元数据,业务只用域模型字段名。
- 字符串拼 SQL 注入 → 占位参数走起,Predicates 能覆盖的就别手搓。
八、扩展方向:让 ORM 再长点肌肉(按需挑)
- 关系加载:
1:N用懒加载或JOIN视图;避免 N+1。 - 软删除:
deleted_at+ 视图/Predicates 统一过滤。 - 审计字段:
created_by/updated_by,谁改的写清楚。 - 查询构建器:链式
select/where/order/limit,最后编译为Predicates或rawQuery。 - 缓存层:热门读放内存(Map + 版本号),降低游标压力。
- 迁移框架:以“编号脚本”管理 DDL,自动回滚/重跑。
结语|把数据库这摊事儿“制度化”,你会省掉 80% 的疲惫
别再让业务页直接摸 ResultSet、到处 copy ValuesBucket、每次都重新写 WHERE 了。RdbStore 只在数据层露面,ValuesBucket/ResultSet 交给封装,实体元数据一处维护,仓库层帮你跑 CRUD。
当你下次要“给 todos 加个 priority 并排序”时,不用通宵重构——加列、加索引、改一处映射,全项目自动受益。舒服到想给自己点个赞。👏
…
(未完待续)
更多推荐




所有评论(0)