本节摘要:RelationalStore 是 HarmonyOS 的关系型数据库接口,底层 SQLite。本节为备忘录应用建库建表,实现参数化的增删改查与事务,覆盖结果集遍历、SQL 预编译与封装要点。
备忘录是演示结构化存储的标准样本:多条记录、每条有标题内容与时间、要按条件查、增删改都齐。需求定型:notes 表,字段 id(主键自增)、title、content、created_at、updated_at、pinned(是否置顶)。
拿到数据库实例并建表:
import { relationalStore } from '@kit.ArkData'; import { BusinessError } from '@kit.BasicServicesKit'; let rdb: relationalStore.RdbStore | undefined; async function initDb(context: Context): Promise<void> { const config: relationalStore.StoreConfig = { name: 'memo.db', securityLevel: relationalStore.SecurityLevel.S1 }; rdb = await relationalStore.getRdbStore(context, config); const sql = `CREATE TABLE IF NOT EXISTS notes ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, content TEXT, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL, pinned INTEGER DEFAULT 0 )`; await rdb.executeSql(sql); }
两个必讲点。securityLevel 是必填的数据安全等级(S1 到 S4),标识数据的敏感程度,第 6 章讲安全时会再遇到这个概念在别处的呼应;executeSql 执行无结果的语句(建表、删表),返回数据的那类操作走另外的接口。
写 SQL 最忌字符串拼接——除了注入风险,值里带引号直接让语句崩掉。RelationalStore 的增删改一律走参数化,以"插入一条备忘"为例:
async function addNote(title: string, content: string): Promise<number> { const values: relationalStore.ValuesBucket = { 'title': title, 'content': content, 'created_at': Date.now(), 'updated_at': Date.now(), 'pinned': 0 }; const rowId = await rdb?.insert('notes', values); return rowId ?? -1; }
ValuesBucket 是字段到值的映射,语句由框架生成,值与语句分离,注入问题从根上消失。改与删用带条件的版本:
async function updateNote(id: number, title: string): Promise<number> { const values: relationalStore.ValuesBucket = { 'title': title, 'updated_at': Date.now() }; const predicates = new relationalStore.RdbPredicates('notes'); predicates.equalTo('id', id); return await rdb?.update(values, predicates) ?? 0; } async function removeNote(id: number): Promise<number> { const predicates = new relationalStore.RdbPredicates('notes'); predicates.equalTo('id', id); return await rdb?.delete(predicates) ?? 0; }
RdbPredicates 是条件构造器:equalTo、like、orderByDesc 等方法链式拼出查询条件,不必手写 WHERE 字符串。查询用 query 接口拿结果集:
async function listNotes(keyword: string): Promise<Note[]> { const predicates = new relationalStore.RdbPredicates('notes'); if (keyword) { predicates.like('title', `%${keyword}%`) } predicates.orderByDesc('pinned').orderByDesc('updated_at'); const columns = ['id', 'title', 'content', 'updated_at', 'pinned']; const resultSet = await rdb?.query(predicates, columns); const notes: Note[] = []; if (resultSet) { while (resultSet.goToNextRow()) { notes.push({ id: resultSet.getLong(resultSet.getColumnIndex('id')), title: resultSet.getString(resultSet.getColumnIndex('title')), content: resultSet.getString(resultSet.getColumnIndex('content')), updatedAt: resultSet.getLong(resultSet.getColumnIndex('updated_at')), pinned: resultSet.getLong(resultSet.getColumnIndex('pinned')) === 1 }); } resultSet.close(); } return notes; } interface Note { id: number title: string content: string updatedAt: number pinned: boolean }
结果集模型是游标式:goToNextRow 逐行前进,getColumnIndex 按列名取下标再按类型取值,用完必须 close。类型要对准建表类型——INTEGER 用 getLong、TEXT 用 getString,类型错位在部分场景能读到但不保证,别赌。
备忘录的"批量删除并写一条操作日志"应该同生共死。数据库层面的保证是事务:
async function clearPinnedWithLog(): Promise<void> { try { rdb?.beginTransaction(); const predicates = new relationalStore.RdbPredicates('notes'); predicates.equalTo('pinned', 1); await rdb?.delete(predicates); const log: relationalStore.ValuesBucket = { 'title': '[系统] 清空置顶', 'content': '批量操作于本机执行', 'created_at': Date.now(), 'updated_at': Date.now(), 'pinned': 0 }; await rdb?.insert('notes', log); rdb?.commit(); } catch (e) { rdb?.rollBack(); throw e as BusinessError; } }
beginTransaction 与 commit 之间是一个原子单元,中途任何异常 rollBack 全部撤销。事务还有一个被低估的副作用:批量插入包进单个事务比循环裸插快得多——每条独立执行都伴随一次磁盘同步,事务把它们合并,千条数据能差出量级。备忘录的"导入示例数据"就该这么做。
背景:给备忘录接上 5.1 的思路——列表页 aboutToAppear 时 listNotes 灌入 @State,界面增删按钮直连 addNote/removeNote。操作:跑通后故意把 removeNote 的条件改成 like(含下划线的通配陷阱)观察误删;再撤回,改回 equalTo。解读:条件构造器的每个方法都有明确语义,"看起来能跑"的 SQL 未必删的是你以为的那批行;开发期用一个可视化手段(如打日志输出受影响行数)确认影响面是便宜而有效的保险。封装建议:把本节的函数收进一个数据访问模块,页面只跟 Note 数组与函数签名打交道,SQL 细节不出模块——第 6 章网络层的封装会沿用同样的分层思路。
⚠️ 常见坑:结果集忘了 close。短期没有可见后果,游标与资源累积后在长会话里表现为内存缓慢上涨,且极难回溯。团队规范可以约定"谁 query 谁 close,用 try/finally 包住"。
💡 关键直觉:Predicates 构造器是"查询即代码"——条件、排序、分页都是类型安全的方法调用,重构字段名时编译器替你把关,这是手写 SQL 字符串永远给不了的安全感。
本节要点回顾:
本地闭环完成。下一章让应用连上互联网:HTTP 请求、JSON 解析、请求封装,以及随之而来的权限申请与网络安全实践。