把关系型数据库当成简单的 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 去做逐步升级。

持久化是一个长期的事情,不要在代码里留“既然出错了那就清空数据”的退路。用户的数据比你的代码更值钱。稳扎稳打地控制好事务和游标,你的应用才不会在数据量膨胀后轰然倒塌。下一讲,我们来解决网络层那些让人头疼的重复请求和拦截问题。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐