文章目录
-
-
- 前言
- [一、 数据库的创建与升降级机制](#一、 数据库的创建与升降级机制)
- [二、 优雅的 CRUD 谓词 (Predicates) 与值桶 (ValuesBucket)](#二、 优雅的 CRUD 谓词 (Predicates) 与值桶 (ValuesBucket))
- [三、 事务处理:批量操作的性能救星](#三、 事务处理:批量操作的性能救星)
- [四、 总结与最佳实践](#四、 总结与最佳实践)
-
前言
在上一篇文章中,我们介绍了用户首选项(Preferences),它非常适合用来存储"夜间模式开关"或"字体大小"这类简单的配置项。
但是,当我们的应用需求升级,比如要开发一个"备忘录"应用,里面有成百上千条笔记,每条笔记都有标题、内容、创建时间和是否置顶的状态,甚至用户还希望能够通过关键字搜索笔记或者按时间排序。这时候,Preferences 就显得力不从心了。如果我们把几千条数据全部读到内存里再进行过滤,性能会瞬间崩塌。
为了解决结构化大数据的存储与查询问题,鸿蒙 HarmonyOS 6 (API 20) 提供了强大的 关系型数据库 (RDB) 。它的底层是大家非常熟悉的 SQLite,但在 API 层面,ArkUI 封装了一套面向对象的 TS 接口,让我们无需编写繁琐的 SQL 语句就能轻松操作数据。同时,它对事务(Transaction)的原生支持,也为批量数据操作的性能和一致性提供了坚实保障。
今天,我们就来深入这一层,看看如何像操作内存数组一样优雅地操作数据库。

一、 数据库的创建与升降级机制
在使用 RDB 之前,我们首先要建立与磁盘数据库文件的连接。这不仅仅是打开一个文件那么简单,它涉及到一个关键的生命周期管理:创建(onCreate) 与 升级(onUpgrade) 。在鸿蒙的 relationalStore 模块中,我们需要配置一个 StoreConfig 对象,指明数据库的文件名和安全等级。
当应用第一次安装并启动时,系统发现本地没有数据库文件,就会触发 onCreate 回调。在这里,是我们执行建表语句(CREATE TABLE)的最佳时机。我们通常会定义好标准的 SQL 字符串,比如创建一个 notes 表,包含 id(主键)、title(标题)、content(内容)等字段。
随着应用版本的迭代,数据库结构往往也会发生变化。比如 v2.0 版本我们需要给笔记增加一个置顶功能,这就需要在表中新增一个 is_pinned 字段。这时候,我们只需要将 StoreConfig 中的版本号从 1 改为 2。当用户更新应用后首次启动,RDB 会检测到版本号不一致,从而触发 onUpgrade 回调。我们需要在这个回调里执行 ALTER TABLE 语句来修改表结构。这种版本控制机制,保证了我们的应用在不断迭代中,用户的老数据依然能平滑迁移,不会丢失。
二、 优雅的 CRUD 谓词 (Predicates) 与值桶 (ValuesBucket)
在传统的后端开发中,我们习惯了手写 INSERT INTO table ... 或者 SELECT * FROM table WHERE ...。但在鸿蒙开发中,为了避免 SQL 注入风险并利用 TypeScript 的类型检查,我们使用 ValuesBucket 和 RDBPredicates 来代替裸写 SQL。
ValuesBucket (值桶)主要用于插入和更新操作。它本质上是一个键值对对象,Key 是数据库的列名,Value 是我们要存的数据。例如 const note = { title: '会议纪要', content: '...' },我们直接把这个对象传给 insert 方法即可。
而 RDBPredicates (谓词)则是查询和删除操作的灵魂。它就像是一个构建查询条件的工厂。假设我们要查询"所有标题包含'会议'且按时间倒序排列"的笔记,我们不需要拼凑 SQL 字符串,而是链式调用:new RdbPredicates('notes').like('title', '%会议%').orderByDesc('created_time')。这种面向对象的查询方式,不仅代码可读性极高,而且在编译阶段就能帮我们规避很多低级的语法错误。
三、 事务处理:批量操作的性能救星
试想这样一个场景:你需要从服务器同步 1000 条历史笔记到本地。如果你在一个 ForEach 循环里调用 1000 次 insert 方法,你会发现应用界面卡顿严重,写入速度慢得惊人。这是因为每一次 insert 操作,底层 SQLite 都会默认开启并提交一个事务,伴随着一次完整的磁盘 I/O。1000 次操作就是 1000 次磁盘读写,这在移动设备上是极大的开销。
为了解决这个问题,我们需要手动管理 事务 (Transaction) 。我们可以调用 beginTransaction() 开启事务,然后执行这 1000 次插入操作。此时,所有的修改都暂时保存在内存缓冲区中。当循环结束后,我们调用 commit(),系统才会一次性将所有数据写入磁盘。如果中间发生了错误,我们可以调用 rollBack(),数据会瞬间回滚到操作前的状态,保证数据的 原子性。在实测中,使用事务进行批量插入,性能通常能提升一个数量级以上。
import { relationalStore, ValuesBucket } from '@kit.ArkData';
import { common } from '@kit.AbilityKit';
import { promptAction } from '@kit.ArkUI';
import { BusinessError } from '@kit.BasicServicesKit';
// -------------------------------------------------------------
// 1. 定义数据模型
// -------------------------------------------------------------
interface Note {
id: number;
title: string;
content: string;
createTime: number;
}
// 数据库配置
const STORE_CONFIG: relationalStore.StoreConfig = {
name: 'Notes.db',
securityLevel: relationalStore.SecurityLevel.S1,
};
const TABLE_NAME = 'notes';
// 建表 SQL
const SQL_CREATE_TABLE = `
CREATE TABLE IF NOT EXISTS ${TABLE_NAME} (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
content TEXT,
createTime INTEGER
)
`;
// -------------------------------------------------------------
// 2. RDB 管理类 (单例)
// -------------------------------------------------------------
class RdbManager {
private static instance: RdbManager;
private rdbStore: relationalStore.RdbStore | null = null;
private constructor() {}
public static getInstance(): RdbManager {
if (!RdbManager.instance) {
RdbManager.instance = new RdbManager();
}
return RdbManager.instance;
}
/**
* 初始化数据库
*/
public async init(context: common.UIAbilityContext): Promise<void> {
if (this.rdbStore) return;
try {
this.rdbStore = await relationalStore.getRdbStore(context, STORE_CONFIG);
// 初始化建表
// 实际项目中建议使用 version 版本号管理数据库升级 (onUpgrade)
await this.rdbStore.executeSql(SQL_CREATE_TABLE);
console.info('[RdbManager] Initialized success');
} catch (err) {
console.error(`[RdbManager] Init failed: ${JSON.stringify(err)}`);
}
}
/**
* 插入数据
*/
public async insertNote(title: string, content: string): Promise<number> {
if (!this.rdbStore) {
console.error('[RdbManager] Store not initialized');
return -1;
}
// 【严格模式适配】
// ValuesBucket 本质是 Record<string, ValueType>
// 必须确保 value 的类型符合 ArkTS 规范
const valueBucket: ValuesBucket = {
'title': title,
'content': content,
'createTime': Date.now()
};
try {
const rowId = await this.rdbStore.insert(TABLE_NAME, valueBucket);
return rowId;
} catch (err) {
console.error(`[RdbManager] Insert failed: ${JSON.stringify(err)}`);
return -1;
}
}
/**
* 查询所有数据
*/
public async queryAllNotes(): Promise<Note[]> {
if (!this.rdbStore) return [];
const predicates = new relationalStore.RdbPredicates(TABLE_NAME);
predicates.orderByDesc('createTime');
// 定义在 try 外面,以便 finally 中关闭
let resultSet: relationalStore.ResultSet | null = null;
const notes: Note[] = [];
try {
resultSet = await this.rdbStore.query(predicates);
// 遍历结果集
// resultSet.goToNextRow() 返回 boolean
while (resultSet.goToNextRow()) {
const idIndex = resultSet.getColumnIndex('id');
const titleIndex = resultSet.getColumnIndex('title');
const contentIndex = resultSet.getColumnIndex('content');
const timeIndex = resultSet.getColumnIndex('createTime');
notes.push({
// 这里的 getLong/getString 可能会在严格模式下有类型警告,
// 但在当前 API 版本中是标准写法
id: resultSet.getLong(idIndex),
title: resultSet.getString(titleIndex),
content: resultSet.getString(contentIndex),
createTime: resultSet.getLong(timeIndex)
});
}
} catch (err) {
console.error(`[RdbManager] Query failed: ${JSON.stringify(err)}`);
} finally {
// 【关键修复】确保资源释放,防止内存泄漏
if (resultSet) {
resultSet.close();
}
}
return notes;
}
/**
* 删除数据
*/
public async deleteNote(id: number): Promise<number> {
if (!this.rdbStore) return -1;
const predicates = new relationalStore.RdbPredicates(TABLE_NAME);
predicates.equalTo('id', id);
try {
return await this.rdbStore.delete(predicates);
} catch (err) {
console.error(`[RdbManager] Delete failed: ${JSON.stringify(err)}`);
return -1;
}
}
}
export const rdbManager = RdbManager.getInstance();
// -------------------------------------------------------------
// 3. 页面 UI 实现
// -------------------------------------------------------------
@Entry
@Component
struct RdbDemoPage {
@State noteList: Note[] = [];
@State newNoteTitle: string = '';
async aboutToAppear() {
try {
const context = getContext(this) as common.UIAbilityContext;
await rdbManager.init(context);
await this.refreshList();
} catch (error) {
console.error(`[Page] Init failed: ${JSON.stringify(error)}`);
}
}
async refreshList() {
this.noteList = await rdbManager.queryAllNotes();
}
build() {
Column() {
// 标题
Text('本地数据库 RDB')
.fontSize(24)
.fontWeight(FontWeight.Bold)
.margin({ top: 40, bottom: 20 })
// 输入区
Row({ space: 10 }) {
TextInput({ text: this.newNoteTitle, placeholder: '输入笔记标题...' })
.layoutWeight(1)
.height(40)
.backgroundColor(Color.White)
.borderRadius(8)
.onChange((value) => {
this.newNoteTitle = value;
})
Button('添加')
.height(40)
.backgroundColor('#0A59F7')
.onClick(async () => {
if (!this.newNoteTitle.trim()) {
promptAction.showToast({ message: '标题不能为空' });
return;
}
// 插入并刷新
await rdbManager.insertNote(this.newNoteTitle, '暂无详细内容');
this.newNoteTitle = ''; // 清空输入框
await this.refreshList();
promptAction.showToast({ message: '保存成功' });
})
}
.width('90%')
.margin({ bottom: 20 })
// 列表区
List({ space: 12 }) {
ForEach(this.noteList, (item: Note) => {
ListItem() {
Row() {
Column() {
Text(item.title)
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#333')
Text(new Date(item.createTime).toLocaleString())
.fontSize(12)
.fontColor('#999')
.margin({ top: 4 })
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
// 删除按钮
Button('删除')
.type(ButtonType.Normal)
.backgroundColor('#FF4040')
.fontColor(Color.White)
.fontSize(12)
.borderRadius(6)
.padding({ left: 12, right: 12, top: 6, bottom: 6 })
.onClick(async () => {
await rdbManager.deleteNote(item.id);
await this.refreshList();
promptAction.showToast({ message: '已删除' });
})
}
.width('100%')
.padding(16)
.backgroundColor(Color.White)
.borderRadius(12)
.shadow({ radius: 4, color: '#1A000000', offsetY: 2 })
}
}, (item: Note) => JSON.stringify(item)) // 使用稳健的键生成策略
}
.layoutWeight(1)
.width('95%')
.scrollBar(BarState.Auto)
// 空状态提示
if (this.noteList.length === 0) {
Column() {
Text('暂无笔记')
.fontSize(16)
.fontColor('#999')
.margin({ top: 60 })
}
}
}
.width('100%')
.height('100%')
.backgroundColor('#F1F3F5')
}
}

四、 总结与最佳实践
关系型数据库 RDB 是鸿蒙应用处理复杂数据的基石。它填补了 Preferences 和文件存储之间的空白,为我们提供了结构化查询的能力。
在实际开发中,建议将 RDB 的操作封装为一个单例的 DatabaseManager ,对外暴露明确的业务方法(如 addNote, getNoteList),而将底层的 Predicates 构建和 SQL 执行细节隐藏起来。同时,务必注意所有数据库操作都是异步的(Promise),请确保在 UI 层面做好 Loading 状态的管理,避免阻塞主线程。