news 2026/5/10 0:15:06

鸿蒙 HarmonyOS 6 | 逻辑核心 (06):本地 关系型数据库 (RDB) 的 CRUD 与事务处理

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
鸿蒙 HarmonyOS 6 | 逻辑核心 (06):本地 关系型数据库 (RDB) 的 CRUD 与事务处理

文章目录

      • 前言
      • 一、 数据库的创建与升降级机制
      • 二、 优雅的 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 的类型检查,我们使用ValuesBucketRDBPredicates来代替裸写 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 状态的管理,避免阻塞主线程。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/5/9 23:38:16

毕业季来了!2026年降AI工具红黑榜,避坑必看

毕业季来了&#xff01;2026年降AI工具红黑榜&#xff0c;避坑必看 TL;DR&#xff1a;毕业季到了&#xff0c;降AI工具鱼龙混杂&#xff0c;别踩坑&#xff01;红榜推荐&#xff1a;嘎嘎降AI&#xff08;效果最佳&#xff09;、比话降AI&#xff08;保障最强&#xff09;、学术…

作者头像 李华
网站建设 2026/5/7 23:21:42

Kimi生成的内容怎么降AI率?亲测这3款工具最靠谱

Kimi生成的内容怎么降AI率&#xff1f;亲测这3款工具最靠谱 TL;DR Kimi写东西效率高&#xff0c;但AI率也高。亲测3款降AI工具后&#xff0c;比话降AI最靠谱——专门适配知网检测&#xff0c;能把Kimi生成的内容AI率从70%降到10%以下&#xff0c;而且不达标全额退款。 Kimi最…

作者头像 李华
网站建设 2026/4/25 16:52:51

谷歌将生成式AI融入Gmail

来源&#xff1a;维度网 谷歌本月开始在Gmail服务中推出一系列基于生成式AI的新工具&#xff0c;旨在帮助用户更高效地管理收件箱并加快邮件撰写速度。 Gmail用户现在可通过输入问题查找邮件&#xff0c;如询问“上个月遇到的招聘人员姓名”。谷歌还在测试新型收件箱&#xff…

作者头像 李华
网站建设 2026/5/8 5:47:43

Rocky Linux 10.1 64位安装Firebird3.0

1、下载Firebird3.0安装包到Linux系统文件下&#xff0c;并解压该文件&#xff0c;进入解压后的文件夹执行 ./install2、会遇到报错Please install required library ‘libtommath’ before firebird, after it repeat firebird install&#xff0c;根据提示需要先安装依赖libt…

作者头像 李华
网站建设 2026/5/2 3:59:32

供应KEYSIGHT E5071C矢量网络分析仪

供应KEYSIGHT E5071C矢量网络分析仪E5071C网络分析仪具有广泛的频率范围和众多功能&#xff0c;在同类产品中具有高的射频性能和快的测试速度。它是制造工程师和研发工程师测量9 kHz至8.5 GHz射频元器件和电路的工具新款 20 GHz 选件可将 E5071C ENA 系列网络分析仪的频率范围扩…

作者头像 李华
网站建设 2026/5/9 1:22:18

安捷伦E8364C矢量网络分析仪E8364B

安捷伦E8364C矢量网络分析仪E8364BAgilent E8364C PNA 微波网络分析仪回收租售Agilent E8364C PNA 微波网络分析仪10 MHz 至 50 GHz主要特性与技术指标10 MHz 至 50 GHz104 dB 的动态范围和 <0.006 dB 的迹线噪声<26 微秒/点的测量速度&#xff0c;32 个通道&#xff0c;…

作者头像 李华