很多鸿蒙应用在数据层的用法停留在"建表、增删改查"这一层。功能能跑,但一旦遇到批量写入性能差、升级后表结构对不上、敏感数据明文落盘这三类问题,基础用法就不够了。本文聚焦 relationalStore 的三个高级能力:事务(Transaction)、版本迁移(Migration) 和 数据库加密(Encrypt),给出可直接落地的封装方案与实测数据。
在讲事务之前,先弄清楚 relationalStore 每次写入到底发生了什么,否则很难理解为什么批量插入必须用事务。
relationalStore 底层基于 SQLite,默认开启 WAL(Write-Ahead Logging) 日志模式。一次 insert 调用的完整路径是:
fsync,确保日志落盘;关键在第 4 步:每一个隐式事务都要付出一次 fsync 的代价。如果你循环调用 1000 次 insert,就是 1000 个独立的隐式事务、1000 次 fsync。而 fsync 是整条链路里最慢的操作(闪存上通常毫秒级)。把 1000 次插入包进一个显式事务,fsync 就只发生一次——这就是事务带来数量级性能差异的根本原因,不是"玄学优化",是 I/O 模型决定的。
WAL 模式还有一个特性值得知道:读写不互斥。读操作走主库文件 + WAL 快照,写操作追加 WAL,因此 UI 线程的查询不会被后台批量写入卡住。但这不代表可以滥用并发写——写与写之间仍然是串行的,多个写事务会排队。
relationalStore 提供 beginTransaction / commit / rollBack 三件套(API 12+ 推荐使用 createTransaction 获取独立事务对象,避免多线程下共享连接状态混乱):
import { relationalStore } from '@kit.ArkData';
async function batchInsert(store: relationalStore.RdbStore,
rows: relationalStore.ValuesBucket[]): Promise<void> {
store.beginTransaction();
try {
for (const row of rows) {
await store.insert('t_note', row);
}
store.commit();
} catch (err) {
store.rollBack(); // 任何一条失败,整批回滚
throw err;
}
}新版事务对象把作用域收敛到对象本身,语义更清晰,也支持指定事务类型:
async function batchInsertV2(store: relationalStore.RdbStore,
rows: relationalStore.ValuesBucket[]): Promise<void> {
const txn = await store.createTransaction({
transactionType: relationalStore.TransactionType.IMMEDIATE
});
try {
for (const row of rows) {
await txn.insert('t_note', row);
}
await txn.commit();
} catch (err) {
await txn.rollback();
throw err;
}
}三种事务类型的选型:
批量写入统一用 IMMEDIATE,把锁冲突暴露在事务开始处,失败重试的代价最小。
测试环境:真机,1000 条记录(每条 4 个字段),三种写法各跑 5 次取中位数:
写法 | 耗时 | 相对性能 |
|---|---|---|
循环裸 insert(1000 个隐式事务) | 4870ms | 1x |
显式事务包裹 1000 次 insert | 312ms | ~15.6x |
事务 + batchInsert 接口 | 89ms | ~54.7x |
结论很直接:批量场景必须用事务,能用 batchInsert 就不要循环单条插。batchInsert 在 Native 层复用编译好的语句,省掉了 1000 次 ArkTS↔Native 跨语言开销,所以比"事务+循环"还要快数倍。
relationalStore 的 getRdbStore 配置里有 version 字段,但它不像 Android Room 那样提供 onUpgrade 回调链,迁移逻辑需要自己管理。裸写很容易出现"跨版本升级漏执行中间迁移"的事故。
推荐把每一步迁移写成独立脚本,按版本号顺序执行:
type Migration = {
from: number;
to: number;
run: (store: relationalStore.RdbStore) => Promise<void>;
};
const MIGRATIONS: Migration[] = [
{
from: 1, to: 2,
run: async (s) => {
// v2:笔记表增加标签字段
await s.executeSql('ALTER TABLE t_note ADD COLUMN tag TEXT DEFAULT ""');
}
},
{
from: 2, to: 3,
run: async (s) => {
// v3:拆分归档表 + 建索引
await s.executeSql(
`CREATE TABLE IF NOT EXISTS t_note_archive (
id INTEGER PRIMARY KEY AUTOINCREMENT,
note_id INTEGER NOT NULL,
archived_at INTEGER NOT NULL)`);
await s.executeSql(
'CREATE INDEX IF NOT EXISTS idx_note_tag ON t_note(tag)');
}
}
];用 SQLite 自带的 PRAGMA user_version 存储当前结构版本,迁移全程包在事务里,任何一步失败整体回滚,保证数据库不会停在"半迁移"状态:
async function migrate(store: relationalStore.RdbStore, target: number): Promise<void> {
const rs = await store.querySql('PRAGMA user_version');
rs.goToFirstRow();
let current = rs.getLong(0);
rs.close();
if (current >= target) { return; }
store.beginTransaction();
try {
while (current < target) {
const step = MIGRATIONS.find(m => m.from === current);
if (!step) {
throw new Error(`missing migration from v${current}`);
}
await step.run(store);
current = step.to;
}
await store.executeSql(`PRAGMA user_version = ${target}`);
store.commit();
} catch (err) {
store.rollBack();
throw err;
}
}这套注册表模式的好处:
一个容易踩的坑:SQLite 的 ALTER TABLE 能力有限(不支持 DROP COLUMN 的旧引擎、不支持修改列类型)。需要"改列"时的标准做法是建新表 → 拷数据 → 删旧表 → 重命名,这四步同样要包在迁移事务里。
relationalStore 的加密是配置级能力,创建时声明即可:
const STORE_CONFIG: relationalStore.StoreConfig = {
name: 'notes.db',
securityLevel: relationalStore.SecurityLevel.S3,
encrypt: true // 落盘加密
};
const store = await relationalStore.getRdbStore(context, STORE_CONFIG);密钥由系统托管(结合 HUKS),应用层不接触密钥本体,也就不存在"密钥硬编码被逆向"的问题。这比自己用 SQLCipher 管密钥安全得多。
注意两点:
同样的 1000 条批量插入 + 全表查询,明文库 vs 加密库:
操作 | 明文库 | 加密库 | 损耗 |
|---|---|---|---|
batchInsert 1000 条 | 89ms | 103ms | ~16% |
全表扫描查询 1000 条 | 21ms | 26ms | ~24% |
冷启动首次打开库 | 11ms | 19ms | ~73% |
结论:读写损耗在 20% 上下,对绝大多数业务无感;首次打开涉及密钥派生所以损耗比例高,但绝对值仍在 20ms 内。敏感数据(用户身份、聊天记录、健康数据)没有理由不开加密;纯缓存类数据可以不开,省下这点开销。
把三个能力拼起来,数据层初始化的标准流程是:
export class Database {
private static store: relationalStore.RdbStore | null = null;
private static readonly VERSION = 3;
static async init(context: Context): Promise<relationalStore.RdbStore> {
if (Database.store) { return Database.store; }
const s = await relationalStore.getRdbStore(context, {
name: 'app.db',
securityLevel: relationalStore.SecurityLevel.S3,
encrypt: true
});
await s.executeSql(
`CREATE TABLE IF NOT EXISTS t_note (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
content TEXT DEFAULT '',
tag TEXT DEFAULT '',
created_at INTEGER NOT NULL)`);
await migrate(s, Database.VERSION);
Database.store = s;
return s;
}
}三条工程原则收个尾:
batchInsert;不确定要不要事务时,答案是"要"。数据层是应用里最不允许"先跑起来再说"的模块。事务保性能与一致性、迁移保升级安全、加密保数据安全,三者都配齐,这一层才算真正可靠。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。