HarmonyOS 关系型数据库 RDB 数据持久化(ArkTS)实战:建库建表、CRUD、事务、FTS、性能优化,一篇搞懂
1. 场景:为什么要用关系型数据库(RDB)?鸿蒙开发者第四期活动
如果你的数据像"表格"一样,字段之间有明确对应关系,比如:
- 班级学生信息:姓名、学号、各科成绩
- 公司员工信息:姓名、工号、职位、部门
- App 里的"错题本/笔记/订单/清单":都有筛选、排序、分页需求
这类数据的特点是:结构清晰、字段多、查询条件复杂 (按学号查、按成绩排序、按日期筛选等)。
这时候 KV-Store(键值)就不舒服了,RDB 才是正解。
RDB 底层基于 SQLite,支持事务、索引、视图、触发器、外键、预编译 SQL、参数化查询等能力,适合做"正经业务数据"的持久化。
2. 必懂概念:谓词 & 结果集
2.1 谓词(RdbPredicates)
你可以理解成:"我想改/删/查哪些行"的条件表达器 。
比如:NAME = 'Lisa'、score > 90、id in (...)。
2.2 结果集(ResultSet)
查询返回的数据不是一次性给你数组,而是一个"游标":
- 默认指向
-1 goToNextRow()才会移动到下一行- 用完一定要
close(),不然容易内存占用飙升
3. 约束和坑点
这些是官方重点强调的,实际开发也很常见:
- 单次查询建议不超过 5000 条,否则可能卡死
- 大查询建议放到 TaskPool 里做
- SQL 拼接尽量简洁、合理分页/分批
- 同一时间只能有一个写操作(写连接不会动态扩充)
- 单条数据建议不要超过 2MB:超过可能"插入成功但读取失败"
- ArkTS 支持的基本类型:
number / string / boolean / 二进制(Uint8Array) / BigInt(用 UNLIMITED INT) - 默认日志模式 WAL ,落盘方式 FULL
- 应用卸载后:数据库文件和临时文件(
-wal/-shm)会被清理
4. 实战案例:做一个"学生表"并完成全套 CRUD(Stage 模型,ArkTS)
我用一个最经典的案例:学生信息管理。
4.1 表结构设计
表:STUDENT
字段建议:
ID:主键自增NO:学号(唯一)NAME:姓名MATH:数学成绩ENGLISH:英语成绩UPDATED_AT:更新时间(时间戳,便于排序)
建表 SQL:
ts
const SQL_CREATE_STUDENT_TABLE =
`CREATE TABLE IF NOT EXISTS STUDENT (
ID INTEGER PRIMARY KEY AUTOINCREMENT,
NO TEXT NOT NULL UNIQUE,
NAME TEXT NOT NULL,
MATH INTEGER,
ENGLISH INTEGER,
UPDATED_AT INTEGER
)`;
5. 我推荐的项目结构(不把数据库写死在页面里)
很多人写 demo 喜欢把 getRdbStore 写在 UIAbility 里,能跑但不利于维护。
我更推荐:RdbManager(单例)+ DAO(数据访问层)。
5.1 RdbManager:负责"拿到 store + 建表 + 版本管理"
ts
import { relationalStore } from '@kit.ArkData';
import { BusinessError } from '@kit.BasicServicesKit';
const DB_NAME = 'Student.db';
const DB_VERSION = 1;
export class RdbManager {
private static instance: RdbManager;
private store?: relationalStore.RdbStore;
static getInstance(): RdbManager {
if (!RdbManager.instance) {
RdbManager.instance = new RdbManager();
}
return RdbManager.instance;
}
async init(context: Context): Promise<relationalStore.RdbStore> {
if (this.store) return this.store;
const config: relationalStore.StoreConfig = {
name: DB_NAME,
securityLevel: relationalStore.SecurityLevel.S3,
encrypt: false,
isReadOnly: false,
};
this.store = await new Promise((resolve, reject) => {
relationalStore.getRdbStore(context, config, (err, store) => {
if (err) reject(err);
else resolve(store);
});
});
// 首次创建数据库时 version = 0
if (this.store.version === 0) {
await this.store.execute(SQL_CREATE_STUDENT_TABLE);
this.store.version = DB_VERSION;
}
return this.store;
}
getStore(): relationalStore.RdbStore {
if (!this.store) {
throw new Error('RdbStore not initialized. Call init() first.');
}
return this.store;
}
}
:同一个数据库名,但不同 context 可能会产生多个数据库实例,所以建议统一在一个入口初始化(比如 EntryAbility / Application 启动时)。
6. StudentDao:增删改查(CRUD)
6.1 插入数据 insert()
ts
import { relationalStore } from '@kit.ArkData';
import { BusinessError } from '@kit.BasicServicesKit';
export interface Student {
no: string;
name: string;
math?: number;
english?: number;
updatedAt: number;
}
export class StudentDao {
constructor(private store: relationalStore.RdbStore) {}
async insertStudent(s: Student): Promise<number> {
const values: relationalStore.ValuesBucket = {
NO: s.no,
NAME: s.name,
MATH: s.math ?? null,
ENGLISH: s.english ?? null,
UPDATED_AT: s.updatedAt
};
try {
// 冲突策略:学号唯一,重复就替换(你也可以改成 ON_CONFLICT_ABORT)
return await this.store.insert('STUDENT', values, relationalStore.ConflictResolution.ON_CONFLICT_REPLACE);
} catch (e) {
const err = e as BusinessError;
console.error(`insertStudent failed, code=${err.code}, msg=${err.message}`);
throw err;
}
}
}
官方有个点很好用:RDB 不需要 flush,insert 就直接落盘了。你博客可以强调一下。
6.2 修改 update()(用谓词锁定行)
ts
async updateScore(no: string, math: number, english: number): Promise<number> {
const values: relationalStore.ValuesBucket = {
MATH: math,
ENGLISH: english,
UPDATED_AT: Date.now()
};
const predicates = new relationalStore.RdbPredicates('STUDENT');
predicates.equalTo('NO', no);
return await new Promise((resolve, reject) => {
this.store.update(values, predicates, (err, rows) => {
if (err) reject(err);
else resolve(rows);
});
});
}
6.3 删除 delete()
async deleteByNo(no: string): Promise<number> {
const predicates = new relationalStore.RdbPredicates('STUDENT');
predicates.equalTo('NO', no);
return await new Promise((resolve, reject) => {
this.store.delete(predicates, (err, rows) => {
if (err) reject(err);
else resolve(rows);
});
});
}
6.4 查询 query() + ResultSet 遍历(重点:记得 close)
ts
async queryByNo(no: string): Promise<Student | null> {
const predicates = new relationalStore.RdbPredicates('STUDENT');
predicates.equalTo('NO', no);
const rs = await new Promise<relationalStore.ResultSet>((resolve, reject) => {
this.store.query(predicates, ['NO', 'NAME', 'MATH', 'ENGLISH', 'UPDATED_AT'], (err, resultSet) => {
if (err) reject(err);
else resolve(resultSet);
});
});
try {
if (rs.goToNextRow()) {
return {
no: rs.getString(rs.getColumnIndex('NO')),
name: rs.getString(rs.getColumnIndex('NAME')),
math: rs.getLong(rs.getColumnIndex('MATH')),
english: rs.getLong(rs.getColumnIndex('ENGLISH')),
updatedAt: rs.getLong(rs.getColumnIndex('UPDATED_AT')),
};
}
return null;
} finally {
rs.close(); // ✅ 非常重要
}
}
7. 事务:批量写入/更新一定要用(性能 + 原子性)
比如:一次导入 1000 个学生成绩,推荐写法:
ts
async importStudents(list: Student[]): Promise<void> {
const tx = await this.store.createTransaction();
try {
for (const s of list) {
await tx.insert('STUDENT', {
NO: s.no,
NAME: s.name,
MATH: s.math ?? null,
ENGLISH: s.english ?? null,
UPDATED_AT: s.updatedAt
}, relationalStore.ConflictResolution.ON_CONFLICT_REPLACE);
}
await tx.commit();
} catch (e) {
await tx.rollback();
throw e;
}
}
我一般会在博客里写一句"人话":
事务就像"打包提交",要么全成功,要么全失败,同时还能减少频繁落盘带来的耗时。
8. 大数据量查询:放进 TaskPool(避免 UI 卡死)
官方建议:大数据查询放 TaskPool。思路就是:耗时操作不要堵主线程。
你可以在博客里写成"经验结论":
- 单次不要超过 5000 条
- 分页/分批
- TaskPool 异步查,查完再回到 UI 更新
(如果你要我补一段 TaskPool + 分页查询的完整示例,我也能继续给你配好。)
9. FTS 全文检索(中文支持 ICU 分词器)
如果你做"笔记/文章/错题解析"这种场景,全局搜索非常常用。
RDB 支持 FTS,中文分词建议 icu zh_CN。
建 FTS 表:
ts
await this.store.execute(
'CREATE VIRTUAL TABLE IF NOT EXISTS note_fts USING fts4(title, content, tokenize=icu zh_CN)'
);
查询:
ts
const rs = await this.store.querySql(
'SELECT title FROM note_fts WHERE note_fts MATCH ?',
['测试']
);
try {
while (rs.goToNextRow()) {
const title = rs.getValue(rs.getColumnIndex('title'));
console.info(`hit: ${title}`);
}
} finally {
rs.close();
}
10. 数据库异常(14800011)怎么办?
官方提到:数据库在操作/存储中可能出现非预期异常(例如 14800011),需要重建并恢复数据 。
我一般博客里会提醒两句:
- 平时要做 备份机制(手动备份也行)
- 真遇到异常,走"重建 + restore"流程,保证业务能恢复
11. 备份 & 恢复(同路径)
备份:
ts
this.store.backup('Backup.db', (err) => {
if (err) console.error(`backup failed: ${err.code}`);
else console.info('backup success');
});
恢复:
ts
this.store.restore('Backup.db', (err) => {
if (err) console.error(`restore failed: ${err.code}`);
else console.info('restore success');
});
12. 删除数据库(慎用)
ts
import { relationalStore } from '@kit.ArkData';
relationalStore.deleteRdbStore(this.context, 'Student.db', (err) => {
if (err) console.error(`delete failed: ${err.code}`);
else console.info('delete success');
});
13. 最后我给一个"选择建议总结"
- 数据结构复杂、要筛选排序分页 → RelationalStore(RDB)
- 配置开关类 → Preferences
- 业务关系弱、想跨设备更友好 → KV-Store