【鸿蒙原生开发会议随记 Pro】 数据存储架构 RelationalStore 在复杂资产管理中的应用
资产管理的核心在于处理数据之间的关联关系与复杂筛选,例如“查询上个月时长超过 30 分钟的会议”。面对此类需求,关系型数据库 RelationalStore(基于 SQLite)能提供远超 Key-Value 的查询效率与内存管理能力。
文章目录
在上一篇文章中,我们通过 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 的性能开销,这在移动端开发中是非常实用的权衡。
更多推荐





所有评论(0)