系列文章《鸿蒙开发从入门到专家》第 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,但我们可以用装饰器和泛型封装一层,减少样板代码。核心思路:
定义实体类,用装饰器标注表名和字段。
写一个
BaseDao<T>,提供save()、delete()、query()。用反射(或手动映射)把
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,能省掉大量样板代码。
下一篇我们看看文件管理:沙盒目录、文件选择器、以及怎么和系统文档生态打通。