news 2026/7/2 0:17:56

鸿蒙 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/6/29 23:52:01

用比话降AI后还是被检测出来?这3个设置你一定没调对

用比话降AI后还是被检测出来&#xff1f;这3个设置你一定没调对 TL;DR 比话降AI效果不好往往是使用方法不对&#xff1a;没有先做检测定位问题段落、没有选择合适的处理模式、处理后缺少人工复核。本文详解三个关键设置技巧&#xff0c;帮你把降AI效果拉满。 「我用了比话&am…

作者头像 李华
网站建设 2026/6/26 16:40:31

豆包写论文后AI率爆表?5款降重工具实测,比话效果最自然

豆包写论文后AI率爆表&#xff1f;5款降重工具实测&#xff0c;比话效果最自然 TL;DR 用豆包写论文效率是高&#xff0c;但AI率动不动就70%、80%。我实测了5款降AI工具&#xff0c;比话降AI效果最自然——不仅能把AI率降到10%以下&#xff0c;改完之后的文字读起来跟人写的一…

作者头像 李华
网站建设 2026/7/1 12:41:18

愿景严格定义,马宝国四两拨千斤《软件方法》第2章

DDD领域驱动设计批评文集 做强化自测题获得“软件方法建模师”称号 《软件方法》各章合集 2.4 建模步骤A-2 定位系统的愿景 2.4.1 愿景的定义 如果有一个系统规格S&#xff0c;S的开发组织是O1&#xff0c;开发组织负责人是P1&#xff0c;S的目标组织是O2&#xff0c;目标组…

作者头像 李华
网站建设 2026/7/1 4:48:27

三、利润是个估计值

彼得德鲁克(Peter Drucker)有一句名言:利润是企业最主要的评价标准。赢利能力也是对管理者的一种评价方式&#xff1a;是为公司贡献了利润还是减少了赢利? 是每天都在为公司赢利想办法,还是仅仅做了自己的工作,只希望别出什么乱子?如果你不知道如何为赢利贡献点儿什么&#x…

作者头像 李华
网站建设 2026/6/30 10:49:14

【趣闻】AMBA协议移除原先Master/Slave提法,改称Manager/Subordinate

起因是最近回顾一些Arm AMBA系列协议的文档&#xff0c;发现以前提法都是Master/Slave&#xff0c;现在看不到了&#xff0c;都换成了Manager/Subordinate。然后在文档中看到如下内容&#xff1a;看上去是某人读文档时看到Master/Slave的字眼感到哈气了&#xff0c;给arm官方提…

作者头像 李华
网站建设 2026/6/28 22:13:28

比话+人工润色组合拳:论文降AI率的终极保姆级教程

比话人工润色组合拳&#xff1a;论文降AI率的终极保姆级教程 TL;DR 单靠工具或单靠手改都有局限&#xff0c;最有效的降AI方法是「工具处理人工润色」组合。本文详解如何用比话降AI做第一轮处理&#xff0c;再通过人工润色技巧把AI率降到极致&#xff0c;附完整操作流程和实战…

作者头像 李华