鸿蒙 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 状态的管理,避免阻塞主线程。

相关推荐
百结2148 小时前
Mysql数据库操作
数据库·mysql·oracle
keep one's resolveY8 小时前
时区问题解决
数据库
Leinwin8 小时前
OpenClaw 多 Agent 协作框架的并发限制与企业化规避方案痛点直击
java·运维·数据库
qq_417695058 小时前
机器学习与人工智能
jvm·数据库·python
漫随流水8 小时前
旅游推荐系统(view.py)
前端·数据库·python·旅游
ego.iblacat9 小时前
MySQL 服务基础
数据库·mysql
Maverick0610 小时前
Oracle Redo 日志操作手册
数据库·oracle
攒了一袋星辰11 小时前
高并发强一致性顺序号生成系统 -- SequenceGenerator
java·数据库·mysql
W.D.小糊涂11 小时前
gpu服务器安装windows+ubuntu24.04双系统
c语言·开发语言·数据库
云贝教育-郑老师11 小时前
【OceanBase 的多租户架构是怎样的?有什么优势?】
数据库·oceanbase