【鸿蒙原生开发会议随记 Pro】 数据存储架构 RelationalStore 在复杂资产管理中的应用

文章目录

      • [一、 存储选型分析:Key-Value 的局限性与 RDB 的优势](#一、 存储选型分析:Key-Value 的局限性与 RDB 的优势)
      • [二、 数据库架构设计:单表结构与安全级别](#二、 数据库架构设计:单表结构与安全级别)
      • [三、 ORM 映射:TypeScript 接口定义](#三、 ORM 映射:TypeScript 接口定义)
      • [四、 数据库管理器:RdbStore 单例封装](#四、 数据库管理器: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 的核心用法。

  1. 架构优势:相比 Key-Value,RDB 支持复杂条件筛选,且只加载游标对应的数据,大幅降低了内存峰值。
  2. 安全性 :配置 SecurityLevel.S1 确保了后台录音等锁屏场景下的数据写入能力。
  3. 反范式化 :通过将参会人列表 (string[]) 序列化为 JSON 字符串存储,我们在保留数据关联性的同时,避免了多表 Join 的性能开销,这在移动端开发中是非常实用的权衡。
相关推荐
敲敲了个代码2 小时前
多标签页强提醒不重复打扰:从“弹框轰炸”到“共享待处理队列”的实战
java·前端·javascript·面试·架构
Henry Zhu1233 小时前
Qt Model/View架构详解(四):高级特性
开发语言·qt·架构
时光慢煮3 小时前
基于 Flutter × OpenHarmony 的文件管家 —— 构建文件类型分类区域
flutter·华为·开源·openharmony
时光慢煮3 小时前
跨端文件管理:Flutter 与 OpenHarmony 搜索栏实战
flutter·华为·开源·openharmony
djarmy4 小时前
跨平台Flutter 开源鸿蒙开发指南(三):使用thirdParty的dio库实现网络请求 示例
flutter·华为·harmonyos
Miguo94well4 小时前
Flutter框架跨平台鸿蒙开发——护眼提醒APP的开发流程
flutter·华为·harmonyos·鸿蒙
Henry Zhu1235 小时前
Qt Model/View架构详解(五):综合实战项目
开发语言·qt·架构
马士兵教育5 小时前
AI大模型通用智能体项目从原理到落地:Agent Skills 的核心逻辑与中间件 + 动态工具实践方案+架构项目实战!
人工智能·中间件·架构
zilikew6 小时前
Flutter框架跨平台鸿蒙开发——拼图游戏的开发流程
flutter·华为·harmonyos·鸿蒙