文章目录
- 一、 存储选型分析:Key-Value 的局限性与 RDB 的优势
- 二、 数据库架构设计:单表结构与安全级别
- 三、 ORM 映射:TypeScript 接口定义
- 四、 数据库管理器:RdbStore 单例封装
- 五、 实战
- 六、 总结
在上一篇文章中,我们通过 MVVM 模式完成了 UI 与逻辑的分层。今天我们将构建系统的核心数据存储层。这是决定应用后期性能上限的关键环节。
在轻量级场景下,开发者习惯使用 UserPreferences。这种 Key-Value 模式使用便捷,仅需简单的 put 和 get 操作即可完成持久化。但对于会议随记 Pro 这类需要处理大量结构化数据的应用,Key-Value 模式并非良选。
资产管理的核心在于处理数据之间的关联关系与复杂筛选,例如“查询上个月时长超过 30 分钟的会议”。面对此类需求,关系型数据库RelationalStore(基于 SQLite)能提供远超 Key-Value 的查询效率与内存管理能力。
本文将以核心的Meeting表为例,演示如何从零构建一个高性能的本地数据库。
一、 存储选型分析:Key-Value 的局限性与 RDB 的优势
如果强行使用 UserPreferences 存储大量会议记录,通常的做法是将列表序列化为 JSON 字符串进行存储。这种方案在数据量较小时表现尚可,但随着数据增长,性能问题会暴露无遗。
1. 内存与 IO 开销
Key-Value 机制通常是一次性全量加载。假设有 500 条会议记录,每次启动应用都需要读取整个 JSON 字符串并反序列化为对象。这不仅占用大量内存,还会导致主线程 IO 阻塞。若只需修改一条数据的标题,也必须执行“全量读取 -> 内存修改 -> 全量写入”的流程,严重浪费闪存寿命。
2. 查询效率对比
KV 模式下,查询特定条件的会议需要遍历整个数组,时间复杂度为 O(n)。而在 RelationalStore 中,配合索引的 SQL 查询可以将时间复杂度降低至 O(log n),且仅加载符合条件的数据到内存中。
代码对比
KV 模式(全量加载,低效):
// 必须加载所有数据才能进行筛选 const allStr = await preferences.get('meetings', '[]'); const allMeetings = JSON.parse(allStr); const results = allMeetings.filter(m => m.duration > 1800);RDB 模式(谓词查询,高效):
// 仅查询符合条件的数据,底层由 C++ 引擎优化 let predicates = new relationalStore.RdbPredicates('meeting'); predicates.greaterThan('duration', 1800); let resultSet = await rdbStore.query(predicates);
二、 数据库架构设计:单表结构与安全级别
为了快速落地,我们专注于核心实体Meeting(会议)。
1. ID 生成策略
在移动端离线架构中,不建议使用自增 ID(Auto Increment)。自增 ID 在多设备数据合并时极易产生冲突。推荐使用UUID字符串作为主键,确保数据的全局唯一性。
2. 安全级别配置
鸿蒙系统对数据库文件有严格的安全分级。默认的S3级别在锁屏后文件会被加密锁定,无法读写。由于会议应用支持后台录音,用户可能在锁屏状态下结束会议并写入数据库,因此必须将安全级别设置为S1(低安全级别,允许锁屏读写)。
3. 表结构定义
我们定义表名为meeting,包含基础信息以及一个用于存储参会人列表的 JSON 字段。
代码示例
import { relationalStore } from '@kit.ArkData'; // 数据库配置 export const DB_CONFIG: relationalStore.StoreConfig = { name: 'meeting_notes.db', securityLevel: relationalStore.SecurityLevel.S1 // 关键配置:允许锁屏写入 }; // 建表语句 // id: UUID 字符串 // attendee_json: 存储参会人列表的 JSON 字符串,简化多表关联 export const SQL_CREATE_MEETING = ` CREATE TABLE IF NOT EXISTS meeting ( id TEXT PRIMARY KEY, title TEXT NOT NULL, start_time INTEGER, duration INTEGER, audio_path TEXT, attendee_json TEXT, created_at INTEGER ) `;三、 ORM 映射:TypeScript 接口定义
为了在业务代码中获得强类型支持,我们需要定义与数据库表结构对应的 TypeScript 接口。这是手动实现的 ORM(对象关系映射)层。
类型转换说明
数据库中的TEXT类型在接口中可能对应string,也可能对应序列化后的对象(如数组)。在接口定义中,我们应直接定义业务所需的类型,在数据读取层再进行解析。
代码示例
export interface Meeting { id: string; title: string; startTime: number; // 对应数据库 start_time duration: number; // 对应数据库 duration audioPath: string; // 对应数据库 audio_path attendees: string[]; // 对应数据库 attendee_json (需反序列化) createdAt: number; // 对应数据库 created_at }四、 数据库管理器:RdbStore 单例封装
打开数据库连接是一个高耗时操作。我们需要封装一个单例的RdbManager来复用RdbStore实例,并处理数据库的初始化与版本升级逻辑。
初始化逻辑
getRdbStore是一个异步方法。我们在初始化时检查store.version。如果是新安装的应用(version 为 0),则执行建表语句并更新版本号。这为后续的数据库字段变更(Migration)预留了接口。
代码示例
import { relationalStore } from '@kit.ArkData'; import { common } from '@kit.AbilityKit'; export class RdbManager { private static instance: RdbManager; private rdbStore: relationalStore.RdbStore | null = null; public static getInstance(): RdbManager { if (!RdbManager.instance) { RdbManager.instance = new RdbManager(); } return RdbManager.instance; } public async getRdbStore(context: common.UIAbilityContext): Promise<relationalStore.RdbStore> { if (this.rdbStore) { return this.rdbStore; } const config: relationalStore.StoreConfig = { name: 'meeting_notes.db', securityLevel: relationalStore.SecurityLevel.S1, }; this.rdbStore = await relationalStore.getRdbStore(context, config); // 数据库版本控制 if (this.rdbStore.version === 0) { // 执行建表 await this.rdbStore.executeSql(` CREATE TABLE IF NOT EXISTS meeting ( id TEXT PRIMARY KEY, title TEXT NOT NULL, start_time INTEGER, duration INTEGER, audio_path TEXT, attendee_json TEXT, created_at INTEGER ) `); // 更新版本号,避免重复建表 this.rdbStore.version = 1; } return this.rdbStore; } }五、 实战
为了保证代码的连贯性与可运行性,我将上述所有逻辑(配置、管理器、业务操作)整合到了一个完整的Index.ets文件中。你可以直接复制该代码运行,它演示了数据库初始化、插入模拟会议数据、以及查询数据的完整闭环。
import { common } from '@kit.AbilityKit'; import { relationalStore, ValuesBucket } from '@kit.ArkData'; import { util } from '@kit.ArkTS'; // ---------------------------------------------------------------- // 1. 数据库管理类 (模拟单独的文件 RdbManager.ts) // ---------------------------------------------------------------- class RdbManager { private static instance: RdbManager; private rdbStore: relationalStore.RdbStore | null = null; public static getInstance(): RdbManager { if (!RdbManager.instance) { RdbManager.instance = new RdbManager(); } return RdbManager.instance; } public async getRdbStore(context: common.UIAbilityContext): Promise<relationalStore.RdbStore> { if (this.rdbStore) { return this.rdbStore; } const config: relationalStore.StoreConfig = { name: 'meeting_demo.db', securityLevel: relationalStore.SecurityLevel.S1, // 允许锁屏读写 }; this.rdbStore = await relationalStore.getRdbStore(context, config); // 版本控制:初始化表结构 if (this.rdbStore.version === 0) { const sql = ` CREATE TABLE IF NOT EXISTS meeting ( id TEXT PRIMARY KEY, title TEXT, start_time INTEGER, duration INTEGER, attendee_json TEXT, created_at INTEGER ) `; await this.rdbStore.executeSql(sql); this.rdbStore.version = 1; } return this.rdbStore; } } // ---------------------------------------------------------------- // 2. 页面交互逻辑 // ---------------------------------------------------------------- @Entry @Component struct Index { @State message: string = 'RelationalStore 准备就绪'; @State queryResult: string = ''; private context = getContext(this) as common.UIAbilityContext; // 插入一条模拟数据 async insertData() { try { const store = await RdbManager.getInstance().getRdbStore(this.context); // 模拟业务数据 const meetingId = util.generateRandomUUID(true); const attendees = ['Alice', 'Bob', 'Charlie']; const valueBucket: ValuesBucket = { 'id': meetingId, 'title': `产品评审会 ${new Date().toLocaleTimeString()}`, 'start_time': Date.now(), 'duration': 3600, // 1小时 'attendee_json': JSON.stringify(attendees), // 数组序列化存储 'created_at': Date.now() }; await store.insert('meeting', valueBucket); this.message = `插入成功,ID: ${meetingId}`; // 插入后立即自动查询刷新 this.queryData(); } catch (e) { this.message = `插入失败: ${JSON.stringify(e)}`; } } // 查询数据 async queryData() { try { const store = await RdbManager.getInstance().getRdbStore(this.context); // 构建谓词:查询所有会议,按创建时间倒序 let predicates = new relationalStore.RdbPredicates('meeting'); predicates.orderByDesc('created_at'); let resultSet = await store.query(predicates); let log = `共查询到 ${resultSet.rowCount} 条记录:\n`; // 遍历游标 while (resultSet.goToNextRow()) { const title = resultSet.getString(resultSet.getColumnIndex('title')); const duration = resultSet.getLong(resultSet.getColumnIndex('duration')); const attendeesStr = resultSet.getString(resultSet.getColumnIndex('attendee_json')); // 反序列化 JSON const attendees = JSON.parse(attendeesStr) as string[]; log += `----------------\n标题: ${title}\n时长: ${duration}秒\n人员: ${attendees.join(', ')}\n`; } // 务必关闭结果集释放资源 resultSet.close(); this.queryResult = log; } catch (e) { this.queryResult = `查询失败: ${JSON.stringify(e)}`; } } build() { Column() { Text('会议资产管理 (RDB)') .fontSize(24) .fontWeight(FontWeight.Bold) .margin({ top: 30, bottom: 20 }) // 操作区 Row({ space: 15 }) { Button('新建会议') .onClick(() => this.insertData()) Button('查询列表') .backgroundColor('#0A59F7') .onClick(() => this.queryData()) } .margin({ bottom: 20 }) Text(this.message) .fontSize(14) .fontColor('#666') .margin({ bottom: 10 }) // 结果展示区 Scroll() { Text(this.queryResult) .fontSize(14) .fontColor('#333') .padding(15) .backgroundColor('#F0F0F0') .width('90%') .borderRadius(8) } .layoutWeight(1) .width('100%') .align(Alignment.Top) } .width('100%') .height('100%') .padding(15) } }六、 总结
我们通过单表Meeting演示了 RelationalStore 的核心用法。
- 架构优势:相比 Key-Value,RDB 支持复杂条件筛选,且只加载游标对应的数据,大幅降低了内存峰值。
- 安全性:配置
SecurityLevel.S1确保了后台录音等锁屏场景下的数据写入能力。 - 反范式化:通过将参会人列表 (
string[]) 序列化为 JSON 字符串存储,我们在保留数据关联性的同时,避免了多表 Join 的性能开销,这在移动端开发中是非常实用的权衡。