Administrator
发布于 2026-09-01 / 61 阅读
0
0

鸿蒙 RDB 数据库进阶:高级查询、索引优化与轻量 ORM

系列文章《鸿蒙开发从入门到专家》第 23 篇 · 中级

我们在第 5 篇聊过 Preferences 和 RDB 的选型。Preferences 适合轻量配置,一旦数据量大、关系复杂,RDB 是绕不开的。但很多开发者只停留在 insert 和 query 的初级用法,遇到多表关联、分页、搜索就手忙脚乱。

本文把 RDB 的进阶能力拆开讲:高级查询语法、索引设计、以及在不引入第三方库的前提下写一套轻量 ORM。

一、RDB 基础回顾

鸿蒙的关系型数据库基于 SQLite,通过 @kit.RelationalStore 访问。核心对象就三个:

  • relationalStore.getRdbStore():获取数据库实例。

  • rdbStore.executeSql():执行建表、ALTER 等 DDL。

  • rdbStore.insert() / update() / delete() / query():CRUD 操作。

建表示例:

import { relationalStore } from '@kit.RelationalStore';

const STORE_CONFIG: relationalStore.StoreConfig = {
  name: 'note.db',
  securityLevel: relationalStore.SecurityLevel.S1,
};

let rdbStore: relationalStore.RdbStore;

async function initDb(context: Context) {
  rdbStore = await relationalStore.getRdbStore(context, STORE_CONFIG);
  await rdbStore.executeSql(`
    CREATE TABLE IF NOT EXISTS note (
      id INTEGER PRIMARY KEY AUTOINCREMENT,
      title TEXT NOT NULL,
      content TEXT,
      category_id INTEGER,
      create_time INTEGER,
      update_time INTEGER
    )
  `);
  await rdbStore.executeSql(`
    CREATE TABLE IF NOT EXISTS category (
      id INTEGER PRIMARY KEY AUTOINCREMENT,
      name TEXT NOT NULL
    )
  `);
}

注意:securityLevel 必须指定,S1 是普通应用最常用的级别。

二、高级查询:JOIN、聚合与分页

查询是 RDB 的精髓。假设要查“每个分类下的笔记数量”,直接用 JOIN + GROUP BY:

const sql = `
  SELECT c.name, COUNT(n.id) AS count
  FROM category c
  LEFT JOIN note n ON c.id = n.category_id
  GROUP BY c.id
`;
const resultSet = await rdbStore.querySql(sql);
while (resultSet.goToNextRow()) {
  const name = resultSet.getString(resultSet.getColumnIndex('name'));
  const count = resultSet.getLong(resultSet.getColumnIndex('count'));
  console.info(`${name}: ${count}`);
}
resultSet.close();

分页查询用 LIMIT + OFFSET,配合 ORDER BY:

const pageSize = 20;
const pageIndex = 0;
const sql = `
  SELECT * FROM note
  ORDER BY update_time DESC
  LIMIT ${pageSize} OFFSET ${pageIndex * pageSize}
`;

如果要支持关键词搜索,别在 SQL 里直接拼字符串,容易 SQL 注入。RDB 支持参数化查询:

const predicates = new relationalStore.RdbPredicates('note');
predicates.like('title', `%${keyword}%`)
  .or()
  .like('content', `%${keyword}%`)
  .orderByDesc('update_time');

const resultSet = await rdbStore.query(predicates, ['id', 'title', 'update_time']);

RdbPredicates 把查询条件对象化,既安全又易读。

三、索引优化:别让查询变成全表扫描

数据量小的时候怎么写都快。一旦笔记数量上万,没有索引的 LIKE '%关键词%' 会让主线程明显卡顿。

常用索引策略:

  • 高频查询字段加索引:category_id、update_time。

  • 组合索引注意最左前缀:(category_id, update_time) 可以支持按分类再按时间排序,但反过来不行。

  • 全文搜索需求不要用 LIKE,考虑额外建 FTS 表(如果有的话)。

CREATE INDEX IF NOT EXISTS idx_note_category ON note(category_id);
CREATE INDEX IF NOT EXISTS idx_note_update_time ON note(update_time);

索引不是越多越好。每次写入都要维护索引,过多的索引会降低插入和更新性能。一般建议:先通过慢查询定位瓶颈,再有针对性地加索引。

四、轻量 ORM:把表映射成对象

鸿蒙没有官方 ORM,但我们可以用装饰器和泛型封装一层,减少样板代码。核心思路:

  1. 定义实体类,用装饰器标注表名和字段。

  2. 写一个 BaseDao<T>,提供 save()、delete()、query()。

  3. 用反射(或手动映射)把 ResultSet 转成对象。

简单示例:

class Note {
  id?: number;
  title: string = '';
  content: string = '';
  categoryId: number = 0;
  createTime: number = Date.now();
  updateTime: number = Date.now();
}

class NoteDao {
  private table = 'note';

  async insert(note: Note): Promise<number> {
    const valueBucket: relationalStore.ValuesBucket = {
      title: note.title,
      content: note.content,
      category_id: note.categoryId,
      create_time: note.createTime,
      update_time: note.updateTime,
    };
    return await rdbStore.insert(this.table, valueBucket);
  }

  async queryByCategory(categoryId: number): Promise<Note[]> {
    const predicates = new relationalStore.RdbPredicates(this.table);
    predicates.equalTo('category_id', categoryId);
    const resultSet = await rdbStore.query(predicates);
    const list: Note[] = [];
    while (resultSet.goToNextRow()) {
      list.push({
        id: resultSet.getLong(resultSet.getColumnIndex('id')),
        title: resultSet.getString(resultSet.getColumnIndex('title')),
        content: resultSet.getString(resultSet.getColumnIndex('content')),
        categoryId: resultSet.getLong(resultSet.getColumnIndex('category_id')),
        createTime: resultSet.getLong(resultSet.getColumnIndex('create_time')),
        updateTime: resultSet.getLong(resultSet.getColumnIndex('update_time')),
      });
    }
    resultSet.close();
    return list;
  }
}

这套代码看起来朴实,但已经把“对象 - SQL - ResultSet”三层转换抽象掉了。项目小的时候没必要引入重型 ORM,自己封装 200 行代码完全够用。

五、迁移策略:数据库版本升级

应用迭代过程中,表结构一定会变。RDB 通过 version 字段管理版本。升级时在回调里判断旧版本,执行 ALTER 语句:

async function upgradeDb(store: relationalStore.RdbStore, oldVersion: number, newVersion: number) {
  if (oldVersion < 2) {
    await store.executeSql('ALTER TABLE note ADD COLUMN tag TEXT');
  }
  if (oldVersion < 3) {
    await store.executeSql('CREATE INDEX IF NOT EXISTS idx_note_tag ON note(tag)');
  }
}

升级逻辑要幂等,避免重复执行导致报错。复杂的表结构变更建议先建新表、导数据、再删旧表,而不是直接 ALTER。

小结

RDB 是鸿蒙应用的数据中坚。进阶用法记住三点:

  • 复杂查询用 RdbPredicates 或原生 SQL,参数化防止注入。

  • 索引根据查询模式来建,避免过度索引。

  • 没有官方 ORM 时,自己封装一层 DAO,能省掉大量样板代码。

下一篇我们看看文件管理:沙盒目录、文件选择器、以及怎么和系统文档生态打通。


评论