鸿蒙 HarmonyOS 6 | 逻辑核心 (06):本地 关系型数据库 (RDB) 的 CRUD 与事务处理

文章目录

      • 前言
      • [一、 数据库的创建与升降级机制](#一、 数据库的创建与升降级机制)
      • [二、 优雅的 CRUD 谓词 (Predicates) 与值桶 (ValuesBucket)](#二、 优雅的 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 状态的管理,避免阻塞主线程。

相关推荐
世人万千丶2 小时前
鸿蒙跨端框架 Flutter 学习 Day 3:综合实践——多维数据流与实时交互实验室
学习·flutter·华为·交互·harmonyos·鸿蒙
世人万千丶2 小时前
鸿蒙跨端框架 Flutter 学习 Day 3:工程实践——数据模型化:从黑盒 Map 走向强类型 Class
学习·flutter·ui·华为·harmonyos·鸿蒙·鸿蒙系统
龙亘川2 小时前
【课程5.7】代码编写:违建处置指标计算(违建发现率、整改率SQL实现)
数据库·oracle·智慧城市·一网统管平台
松涛和鸣2 小时前
55、ARM与IMX6ULL入门
c语言·arm开发·数据库·单片机·sqlite·html
这儿有一堆花2 小时前
Linux 内网环境构建与配置深度解析
linux·数据库·php
Codeking__3 小时前
Redis——事务
数据库·redis·缓存
IT陈图图3 小时前
基于 Flutter × OpenHarmony 的应用头部信息区域的实现与解析
flutter·华为·openharmony
Codeking__3 小时前
Redis——认识持久化、RDB、AOF
数据库·redis·缓存
Funky_oaNiu3 小时前
Oracle在没有dba权限和表空间对不上和不能用数据泵的情况下迁移
数据库·oracle·dba
阳光九叶草LXGZXJ3 小时前
达梦数据库-报错-06-[-502]OUT OF TEMPORARY DATABASE SPACE(临时表空间不足)
linux·运维·数据库·sql·学习