海量数据持久化(第3讲) RelationalStore 批量写入与事务边界控制
把关系型数据库当成简单的 KV 存储,是性能灾难的开始
很多从 Web 前端转过来的同学,习惯了 localStorage 那种存一个 JSON 字符串的做法,到了鸿蒙上,发现有个 RelationalStore,也随手建个表,把整个对象 JSON.stringify 塞进一个字段里。这种做法在小数据量下能跑,但一旦数据量破万,你搜一个字段就得全表扫,还得把每个大 JSON 拉出来反序列化,卡到怀疑人生。
关系型数据库的精髓在于结构化与索引。但就算你表建对了,在批量导入数据时,如果直接写个 for 循环去调 insert,每一条数据都要经历一次底层的磁盘 IO 和事务提交,这叫“把跑车开进了泥潭”。

批量写入与事务边界
系统接口其实已经给你了 batchInsert,但很多人为了加一些业务逻辑,喜欢用循环调 insert。如果你非要用循环,那必须得包在 beginTransaction 里面。但官方更推荐直接使用 batchInsert。
typescript
import { relationalStore } from '@kit.ArkData';
async function batchSaveRecords(store: relationalStore.RdbStore, records: any[]) {
// 将业务对象转为 ValuesBucket 数组
const buckets: relationalStore.ValuesBucket[] = records.map(r => ({
id: r.id,
title: r.title,
content: r.content,
created_at: r.time
}));
try {
// 开启事务
store.beginTransaction();
// batchInsert 内部会进行优化,比单条 insert 快数十倍
const insertCount = await store.batchInsert('note_table', buckets);
// 提交事务
store.commit();
console.info(`成功插入 ${insertCount} 条记录`);
} catch (e) {
// 发生异常必须回滚,否则数据库可能被锁死
store.rollBack();
console.error(`批量插入失败: ${JSON.stringify(e)}`);
}
}
beginTransaction 和 commit 之间,就是一个坚固的沙箱。要么全成功,要么全失败。batchInsert 返回的 insertCount 能告诉你实际插入了多少条,如果这个数字小于你传入的数组长度,说明遇到了主键冲突导致跳过了。常见错误是事务开启后,在 catch 里忘了 rollBack。这会导致底层的数据库文件被锁住,后续所有的写入操作都会陷入死锁等待。

超大数据的切片与查询优化
如果你要导入十万条数据,即使用了 batchInsert,一次性全塞进去也可能会让内存或 CPU 瞬间拉满。正确的做法是分片处理,比如每 1000 条作为一个批次,执行一次完整的事务提交。
查询也是一样的道理。不要用 select *,用到什么字段查什么字段。尤其是有大文本字段的时候。
typescript
async function queryNotes(store: relationalStore.RdbStore, pageSize: number, pageIndex: number) {
const predicates = new relationalStore.RdbPredicates('note_table');
// 只查需要的列,避免大字段撑爆内存
const columns = ['id', 'title', 'created_at'];
// 分页查询,绝对不能全表拉取
predicates.limitAs(pageSize).offsetAs((pageIndex - 1) * pageSize);
// 按时间倒序
predicates.orderByDesc('created_at');
const resultSet = await store.query(predicates, columns);
const list: any[] = [];
while (resultSet.goToNextRow()) {
list.push({
id: resultSet.getString(resultSet.getColumnIndex('id')),
title: resultSet.getString(resultSet.getColumnIndex('title')),
time: resultSet.getLong(resultSet.getColumnIndex('created_at'))
});
}
// 游标用完必须关闭,释放内存
resultSet.close();
return list;
}
limitAs 和 offsetAs 是实现分页的关键,对于长列表,必须结合 LazyForEach 一起使用。这里最容易踩的坑是忘记 resultSet.close()。游标不关,底层的内存就一直在涨,几次查询之后应用就会 OOM 闪退。执行上述代码,你会看到内存始终维持在一个平稳的低位,即使表里有上百万条数据,每次也只会处理这一页的量。
升级与迁移的克制
数据库表结构变更时,不要暴力删表重建。你要写迁移脚本,用 ALTER TABLE 去加字段。把数据库的版本号存在 Preferences 里,每次启动检查,用 switch case 去做逐步升级。
持久化是一个长期的事情,不要在代码里留“既然出错了那就清空数据”的退路。用户的数据比你的代码更值钱。稳扎稳打地控制好事务和游标,你的应用才不会在数据量膨胀后轰然倒塌。下一讲,我们来解决网络层那些让人头疼的重复请求和拦截问题。
更多推荐
所有评论(0)