HarmonyOS 5 数据持久化:关系型数据库 (RelationalStore)

HarmonyOS 5 数据持久化:关系型数据库 (RelationalStore)

大家好,我是不想掉发的鸿蒙开发工程师 城中的雾,在前两期,我们用首选项存了配置,用 PersistentStorage 存了 UI 状态。但如果老板提了这么个需求:"给我做一个备忘录 App,要能存几千条笔记,还要能按标题搜索,按时间排序,最好还能筛选出已完成的。"这时候你再去用首选项,性能上得不到支持。对于这种结构化、大量、需查询的数据,我们需要请出数据存储常用的------关系型数据库 (RelationalStore)。

1. 它是谁?SQLite 的鸿蒙分身

鸿蒙的 RelationalStore 底层其实就是大家熟悉的 SQLite

如果你用过 SQL,那你已经学会了 80%。它把数据存成一张张"表" (Table),每行是一条数据,每列是一个字段。

什么时候用它?

  • 数据量大:超过 100 条,甚至上万条。
  • 结构复杂:数据包含多个字段(如:ID、标题、内容、时间、作者、状态)。
  • 需要查询 :需要 WHEREORDER BYLIMIT 等操作。

2. 核心概念

在写代码前,先认全这三个核心类:

  1. RdbStore:数据库的大管家。负责建库、删库、执行 SQL。
  2. ValuesBucket:一个"水桶"。插入或更新数据时,先把数据丢进桶里(其实就是个键值对对象)。
  3. RdbPredicates :筛选器。用来构建 SQL 中的 WHERE 子句(比如 equalTo, contains, orderByDesc)。

3. 实战:封装 RdbManager

数据库操作比较重,绝对不能写在 UI 页面里。我们按照标准架构,封装一个单例管理类。

假设我们要通过一个 备忘录 (Note) 功能来演示。

表结构:id (主键), title (标题), content (内容), date (时间戳).

第一步:定义数据模型 (NoteModel.ets)

复制代码
// model/NoteModel.ets
export class NoteModel {
  id: number | null = null;
  title: string;
  content: string;
  date: number;

  constructor(title: string, content: string, date: number, id?: number) {
    this.title = title;
    this.content = content;
    this.date = date;
    if (id) this.id = id;
  }
}

第二步:封装数据库工具类 (RdbManager.ets)

这个类负责处理所有脏活累活:建表、增删改查。

复制代码
// utils/RdbManager.ets
import { relationalStore, ValuesBucket } from '@kit.ArkData';
import { common } from '@kit.AbilityKit';
import { NoteModel } from '../model/NoteModel';
import { BusinessError } from '@kit.BasicServicesKit';

export class RdbManager {
  private static instance: RdbManager;
  private rdbStore: relationalStore.RdbStore | null = null;
  private readonly TABLE_NAME = 'notes';

  // 建表 SQL 语句
  private readonly SQL_CREATE_TABLE = `CREATE TABLE IF NOT EXISTS ${this.TABLE_NAME} (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    title TEXT,
    content TEXT,
    date INTEGER
  )`;

  private constructor() {}

  public static get(): RdbManager {
    if (!RdbManager.instance) {
      RdbManager.instance = new RdbManager();
    }
    return RdbManager.instance;
  }

  /**
   * 初始化数据库
   * @param context ApplicationContext
   */
  init(context: common.Context): Promise<void> {
    return new Promise((resolve, reject) => {
      const STORE_CONFIG: relationalStore.StoreConfig = {
        name: 'MyNotes.db', // 数据库文件名
        securityLevel: relationalStore.SecurityLevel.S1, // 安全等级
      };

      relationalStore.getRdbStore(context, STORE_CONFIG, (err, store) => {
        if (err) {
          console.error(`RdbManager: 初始化失败 code=${err.code}`);
          reject(err);
          return;
        }
        
        // 创建表
        store.executeSql(this.SQL_CREATE_TABLE);
        this.rdbStore = store;
        console.info('RdbManager: 初始化成功');
        resolve();
      });
    });
  }

  /**
   * 插入数据
   */
  async insert(note: NoteModel): Promise<number> {
    if (!this.rdbStore) return -1;
    
    // 1. 组装 ValuesBucket
    const valueBucket: ValuesBucket = {
      title: note.title,
      content: note.content,
      date: note.date
    };

    // 2. 插入
    return await this.rdbStore.insert(this.TABLE_NAME, valueBucket);
  }

  /**
   * 查询所有数据
   */
  async queryAll(): Promise<NoteModel[]> {
    if (!this.rdbStore) return [];

    // 1. 构建查询条件 (查询所有,按时间倒序)
    let predicates = new relationalStore.RdbPredicates(this.TABLE_NAME);
    predicates.orderByDesc('date');

    // 2. 执行查询,得到 ResultSet (结果集游标)
    let resultSet = await this.rdbStore.query(predicates);
    
    // 3. 解析 ResultSet
    let result: NoteModel[] = [];
    while (resultSet.goToNextRow()) {
      // 必须通过列名获取索引,再获取值
      let id = resultSet.getLong(resultSet.getColumnIndex('id'));
      let title = resultSet.getString(resultSet.getColumnIndex('title'));
      let content = resultSet.getString(resultSet.getColumnIndex('content'));
      let date = resultSet.getLong(resultSet.getColumnIndex('date'));
      
      result.push(new NoteModel(title, content, date, id));
    }
    
    // 4. 极其重要:用完必须关闭结果集,否则内存泄漏!
    resultSet.close();
    return result;
  }
  
  /**
   * 根据 ID 删除
   */
  async delete(id: number): Promise<number> {
    if (!this.rdbStore) return -1;
    
    let predicates = new relationalStore.RdbPredicates(this.TABLE_NAME);
    predicates.equalTo('id', id); // WHERE id = ?
    
    return await this.rdbStore.delete(predicates);
  }
}

第三步:数据库初始化

这次我们放在 aboutToAppear 里初始化数据库。

复制代码
  // 页面显示时初始化数据库
  async aboutToAppear() {
    await RdbManager.get().init(this.getUIContext().getHostContext()!);
  }

4. 场景演练:备忘录列表

我们做一个简单的界面:展示列表,点击按钮添加随机笔记,点击单项删除。

复制代码
import { RdbManager } from '../utils/RdbManager';
import { NoteModel } from '../model/NoteModel';
import { promptAction } from '@kit.ArkUI';

@Entry
@Component
struct NotePage {
  @State notes: NoteModel[] = [];

  // 页面显示时加载数据
  async aboutToAppear() {
    this.refreshList();
  }

  async refreshList() {
    this.notes = await RdbManager.get().queryAll();
  }

  build() {
    Column() {
      // 标题栏
      Row() {
        Text('我的备忘录').fontSize(24).fontWeight(FontWeight.Bold)
        Blank()
        Button('+ 新建')
          .onClick(async () => {
            // 模拟插入一条数据
            let newNote = new NoteModel(
              `笔记 ${Math.floor(Math.random() * 100)}`,
              '这是自动生成的测试内容...',
              Date.now()
            );
            await RdbManager.get().insert(newNote);
            promptAction.showToast({ message: '保存成功' });
            this.refreshList(); // 刷新列表
          })
      }
      .width('100%')
      .padding(20)

      // 列表区域
      List({ space: 10 }) {
        ForEach(this.notes, (item: NoteModel) => {
          ListItem() {
            Row() {
              Column({ space: 5 }) {
                Text(item.title).fontSize(18).fontWeight(FontWeight.Bold)
                Text(new Date(item.date).toLocaleString())
                  .fontSize(12).fontColor('#999')
              }
              .alignItems(HorizontalAlign.Start)
              
              Blank()
              
              // 删除按钮
              Button('删除')
                .backgroundColor('#FF4040')
                .fontSize(12)
                .height(30)
                .onClick(async () => {
                  if (item.id) {
                    await RdbManager.get().delete(item.id);
                    this.refreshList();
                  }
                })
            }
            .width('100%')
            .padding(15)
            .backgroundColor('#F5F5F5')
            .borderRadius(10)
          }
        })
      }
      .layoutWeight(1)
      .width('100%')
      .padding({ left: 15, right: 15 })
    }
    .height('100%')
  }
}

5. 进阶:如何做搜索?

RDB 的强大之处在于查询。假设我们要实现"搜索标题包含关键字"的功能。

在 RdbManager 中添加方法

复制代码
  /**
   * 模糊搜索
   * @param keyword 搜索关键字
   */
  async search(keyword: string): Promise<NoteModel[]> {
    if (!this.rdbStore) return [];

    let predicates = new relationalStore.RdbPredicates(this.TABLE_NAME);
    // 核心:使用 contains (相当于 SQL 的 LIKE %keyword%)
    predicates.contains('title', keyword);
    predicates.orderByDesc('date');

    let resultSet = await this.rdbStore.query(predicates);
    // ... 解析 ResultSet 逻辑同上 ...
    // (实际开发中建议把解析逻辑抽离成一个 private 方法)
    return this.parseResultSet(resultSet);
  }

6. 总结与避坑

  1. ResultSet 必须关闭 :这是新手最大的坑!用完 resultSet 一定要调 .close(),否则会导致严重的内存泄漏,甚至导致应用崩溃。
  2. ValuesBucket 类型限制:它只支持基本类型(string, number, boolean, Uint8Array)。如果你想存对象,得转成 JSON 字符串存进去。
  3. 主键 id :建表时建议加上 INTEGER PRIMARY KEY AUTOINCREMENT,这样你就不用操心 ID 生成的问题了。
  4. 异步操作 :RDB 的所有操作(增删改查)都是异步的,记得使用 async/await,不要阻塞 UI 线程。

下一期预告:

现在我们能存配置、能存对象、能存列表了。

但是...

  • "用户下载的 PDF 文件存哪?"
  • "我拍了一张照片,怎么保存到手机相册或者私有目录?"
  • "怎么读取 RawFile 里的初始数据?"

下一篇,我们将进入文件系统的世界,探讨 沙箱文件 (FileIO),揭秘鸿蒙 App 的沙箱存储。

📚 充电时间

如果有想加入鸿蒙生态的大佬们,快来加入鸿蒙认证吧!初高级证书还没获取的,点这里:

🔗 HarmonyOS第一课:官方认证培训

如果您有任何疑问、对文章写的不满意、发现错误或者有更好的方法,欢迎在评论、私信中提出,非常感谢您的支持。

相关推荐
共享家95272 小时前
MySQL -复合查询
数据库·mysql
合方圆~小文2 小时前
双目摄像头在不同距离精度差异
数据库·人工智能·模块测试
计算机毕设VX:Fegn08952 小时前
计算机毕业设计|基于springboot + vue校园招聘系统(源码+数据库+文档)
数据库·vue.js·spring boot·后端·课程设计
航Hang*2 小时前
第3章:复习篇——第3节:数据查询与统计
数据库·笔记·sql·mysql
DBA小马哥2 小时前
金仓数据库:构建国产数据库安全迁移新范式,助力Oracle替代进程
数据库·oracle
星环科技2 小时前
什么是分布式数据库?一文了解分布式数据库
数据库
子榆.2 小时前
Flutter 与开源鸿蒙(OpenHarmony)离线地图与定位实战:无网络也能精准导航
flutter·开源·harmonyos
么么...2 小时前
SQL 学习指南:从零开始掌握DQL结构化查询语言
数据库·经验分享·笔记·sql
ekkcole2 小时前
mysql查看数据库指定字段存在哪个表
数据库·mysql