HarmonyOS 5 数据持久化:关系型数据库 (RelationalStore)
大家好,我是不想掉发的鸿蒙开发工程师 城中的雾,在前两期,我们用首选项存了配置,用 PersistentStorage 存了 UI 状态。但如果老板提了这么个需求:"给我做一个备忘录 App,要能存几千条笔记,还要能按标题搜索,按时间排序,最好还能筛选出已完成的。"这时候你再去用首选项,性能上得不到支持。对于这种结构化、大量、需查询的数据,我们需要请出数据存储常用的------关系型数据库 (RelationalStore)。
1. 它是谁?SQLite 的鸿蒙分身
鸿蒙的 RelationalStore 底层其实就是大家熟悉的 SQLite。
如果你用过 SQL,那你已经学会了 80%。它把数据存成一张张"表" (Table),每行是一条数据,每列是一个字段。
什么时候用它?
- 数据量大:超过 100 条,甚至上万条。
- 结构复杂:数据包含多个字段(如:ID、标题、内容、时间、作者、状态)。
- 需要查询 :需要
WHERE、ORDER BY、LIMIT等操作。
2. 核心概念
在写代码前,先认全这三个核心类:
- RdbStore:数据库的大管家。负责建库、删库、执行 SQL。
- ValuesBucket:一个"水桶"。插入或更新数据时,先把数据丢进桶里(其实就是个键值对对象)。
- RdbPredicates :筛选器。用来构建 SQL 中的
WHERE子句(比如equalTo,contains,orderByDesc)。
3. 实战:封装 RdbManager
数据库操作比较重,绝对不能写在 UI 页面里。我们按照标准架构,封装一个单例管理类。
假设我们要通过一个 备忘录 (Note) 功能来演示。
表结构:id (主键), title (标题), content (内容), date (时间戳).
第一步:定义数据模型 (NoteModel.ets)
// model/NoteModel.ets
export class NoteModel {
id: number | null = null;
title: string;
content: string;
date: number;
constructor(title: string, content: string, date: number, id?: number) {
this.title = title;
this.content = content;
this.date = date;
if (id) this.id = id;
}
}
第二步:封装数据库工具类 (RdbManager.ets)
这个类负责处理所有脏活累活:建表、增删改查。
// utils/RdbManager.ets
import { relationalStore, ValuesBucket } from '@kit.ArkData';
import { common } from '@kit.AbilityKit';
import { NoteModel } from '../model/NoteModel';
import { BusinessError } from '@kit.BasicServicesKit';
export class RdbManager {
private static instance: RdbManager;
private rdbStore: relationalStore.RdbStore | null = null;
private readonly TABLE_NAME = 'notes';
// 建表 SQL 语句
private readonly SQL_CREATE_TABLE = `CREATE TABLE IF NOT EXISTS ${this.TABLE_NAME} (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT,
content TEXT,
date INTEGER
)`;
private constructor() {}
public static get(): RdbManager {
if (!RdbManager.instance) {
RdbManager.instance = new RdbManager();
}
return RdbManager.instance;
}
/**
* 初始化数据库
* @param context ApplicationContext
*/
init(context: common.Context): Promise<void> {
return new Promise((resolve, reject) => {
const STORE_CONFIG: relationalStore.StoreConfig = {
name: 'MyNotes.db', // 数据库文件名
securityLevel: relationalStore.SecurityLevel.S1, // 安全等级
};
relationalStore.getRdbStore(context, STORE_CONFIG, (err, store) => {
if (err) {
console.error(`RdbManager: 初始化失败 code=${err.code}`);
reject(err);
return;
}
// 创建表
store.executeSql(this.SQL_CREATE_TABLE);
this.rdbStore = store;
console.info('RdbManager: 初始化成功');
resolve();
});
});
}
/**
* 插入数据
*/
async insert(note: NoteModel): Promise<number> {
if (!this.rdbStore) return -1;
// 1. 组装 ValuesBucket
const valueBucket: ValuesBucket = {
title: note.title,
content: note.content,
date: note.date
};
// 2. 插入
return await this.rdbStore.insert(this.TABLE_NAME, valueBucket);
}
/**
* 查询所有数据
*/
async queryAll(): Promise<NoteModel[]> {
if (!this.rdbStore) return [];
// 1. 构建查询条件 (查询所有,按时间倒序)
let predicates = new relationalStore.RdbPredicates(this.TABLE_NAME);
predicates.orderByDesc('date');
// 2. 执行查询,得到 ResultSet (结果集游标)
let resultSet = await this.rdbStore.query(predicates);
// 3. 解析 ResultSet
let result: NoteModel[] = [];
while (resultSet.goToNextRow()) {
// 必须通过列名获取索引,再获取值
let id = resultSet.getLong(resultSet.getColumnIndex('id'));
let title = resultSet.getString(resultSet.getColumnIndex('title'));
let content = resultSet.getString(resultSet.getColumnIndex('content'));
let date = resultSet.getLong(resultSet.getColumnIndex('date'));
result.push(new NoteModel(title, content, date, id));
}
// 4. 极其重要:用完必须关闭结果集,否则内存泄漏!
resultSet.close();
return result;
}
/**
* 根据 ID 删除
*/
async delete(id: number): Promise<number> {
if (!this.rdbStore) return -1;
let predicates = new relationalStore.RdbPredicates(this.TABLE_NAME);
predicates.equalTo('id', id); // WHERE id = ?
return await this.rdbStore.delete(predicates);
}
}
第三步:数据库初始化
这次我们放在 aboutToAppear 里初始化数据库。
// 页面显示时初始化数据库
async aboutToAppear() {
await RdbManager.get().init(this.getUIContext().getHostContext()!);
}
4. 场景演练:备忘录列表
我们做一个简单的界面:展示列表,点击按钮添加随机笔记,点击单项删除。

import { RdbManager } from '../utils/RdbManager';
import { NoteModel } from '../model/NoteModel';
import { promptAction } from '@kit.ArkUI';
@Entry
@Component
struct NotePage {
@State notes: NoteModel[] = [];
// 页面显示时加载数据
async aboutToAppear() {
this.refreshList();
}
async refreshList() {
this.notes = await RdbManager.get().queryAll();
}
build() {
Column() {
// 标题栏
Row() {
Text('我的备忘录').fontSize(24).fontWeight(FontWeight.Bold)
Blank()
Button('+ 新建')
.onClick(async () => {
// 模拟插入一条数据
let newNote = new NoteModel(
`笔记 ${Math.floor(Math.random() * 100)}`,
'这是自动生成的测试内容...',
Date.now()
);
await RdbManager.get().insert(newNote);
promptAction.showToast({ message: '保存成功' });
this.refreshList(); // 刷新列表
})
}
.width('100%')
.padding(20)
// 列表区域
List({ space: 10 }) {
ForEach(this.notes, (item: NoteModel) => {
ListItem() {
Row() {
Column({ space: 5 }) {
Text(item.title).fontSize(18).fontWeight(FontWeight.Bold)
Text(new Date(item.date).toLocaleString())
.fontSize(12).fontColor('#999')
}
.alignItems(HorizontalAlign.Start)
Blank()
// 删除按钮
Button('删除')
.backgroundColor('#FF4040')
.fontSize(12)
.height(30)
.onClick(async () => {
if (item.id) {
await RdbManager.get().delete(item.id);
this.refreshList();
}
})
}
.width('100%')
.padding(15)
.backgroundColor('#F5F5F5')
.borderRadius(10)
}
})
}
.layoutWeight(1)
.width('100%')
.padding({ left: 15, right: 15 })
}
.height('100%')
}
}
5. 进阶:如何做搜索?
RDB 的强大之处在于查询。假设我们要实现"搜索标题包含关键字"的功能。
在 RdbManager 中添加方法:
/**
* 模糊搜索
* @param keyword 搜索关键字
*/
async search(keyword: string): Promise<NoteModel[]> {
if (!this.rdbStore) return [];
let predicates = new relationalStore.RdbPredicates(this.TABLE_NAME);
// 核心:使用 contains (相当于 SQL 的 LIKE %keyword%)
predicates.contains('title', keyword);
predicates.orderByDesc('date');
let resultSet = await this.rdbStore.query(predicates);
// ... 解析 ResultSet 逻辑同上 ...
// (实际开发中建议把解析逻辑抽离成一个 private 方法)
return this.parseResultSet(resultSet);
}
6. 总结与避坑
- ResultSet 必须关闭 :这是新手最大的坑!用完
resultSet一定要调.close(),否则会导致严重的内存泄漏,甚至导致应用崩溃。 - ValuesBucket 类型限制:它只支持基本类型(string, number, boolean, Uint8Array)。如果你想存对象,得转成 JSON 字符串存进去。
- 主键 id :建表时建议加上
INTEGER PRIMARY KEY AUTOINCREMENT,这样你就不用操心 ID 生成的问题了。 - 异步操作 :RDB 的所有操作(增删改查)都是异步的,记得使用
async/await,不要阻塞 UI 线程。
下一期预告:
现在我们能存配置、能存对象、能存列表了。
但是...
- "用户下载的 PDF 文件存哪?"
- "我拍了一张照片,怎么保存到手机相册或者私有目录?"
- "怎么读取 RawFile 里的初始数据?"
下一篇,我们将进入文件系统的世界,探讨 沙箱文件 (FileIO),揭秘鸿蒙 App 的沙箱存储。
📚 充电时间
如果有想加入鸿蒙生态的大佬们,快来加入鸿蒙认证吧!初高级证书还没获取的,点这里:
如果您有任何疑问、对文章写的不满意、发现错误或者有更好的方法,欢迎在评论、私信中提出,非常感谢您的支持。