RDB + HarmonyOS 本地持久化策略(第 4 讲):从表结构更替到无损事务迁移
只要涉及到用户数据,就一定会面临版本升级的考验。应用发版,数据库新增了一个字段,或者加了一个索引。如果你在处理 onUpgrade 的逻辑时简单粗暴地来一句 DROP TABLE,那么恭喜你,下次发版大概率会因为用户数据清零而被打一星差评。
关系型数据库(RDB)的版本迁移,是一场没有回头路的走钢丝。今天这讲,我们把数据迁移的安全垫铺好,聊聊怎么做到无损升级。
事务是数据安全的最后一道防线
在 relationalStore 里,当版本号发生变化(比如从 1 改到 2),系统会自动打开数据库并允许你执行修改。很多开发者为了省事,直接执行一串 SQL,一旦中间有一条语句因为语法错误或者字段冲突挂了,用户的数据库就会卡在这个半新半旧的“缝合怪”状态,应用直接白屏崩溃。
必须使用事务。事务保证了 ACID 里的原子性:要么全部执行成功,要么全部回滚到修改前的状态。
typescript
import { relationalStore } from '@kit.ArkData';
async function upgradeDatabase(store: relationalStore.RdbStore) {
// 1. 开启事务
store.beginTransaction();
try {
// 2. 创建一个临时的新表
await store.executeSql(`
CREATE TABLE IF NOT EXISTS UserInfo_New (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT,
age INTEGER,
email TEXT
)
`);
// 3. 将旧表数据导入新表(缺失的 email 字段填入默认值)
await store.executeSql(`
INSERT INTO UserInfo_New (name, age, email)
SELECT name, age, 'default@test.com' FROM UserInfo
`);
// 4. 删除旧表,并将新表重命名
await store.executeSql('DROP TABLE UserInfo');
await store.executeSql('ALTER TABLE UserInfo_New RENAME TO UserInfo');
// 5. 提交事务,正式生效
store.commit();
} catch (err) {
// 6. 任何一步报错,直接回滚,旧数据毫发无损
store.rollBack();
console.error(`数据库升级失败,已回滚。错误信息: ${err.message}`);
}
}
看这段代码,最关键的是 beginTransaction() 和 rollBack()。如果新表的创建或者数据插入失败,回滚操作会让数据库瞬间回到升级前的状态。虽然这次升级失败了,但用户的数据还在,应用不会因为找不到表而崩溃。你可以在下次启动时再尝试重试。
降级安装的兜底策略
这世界上有一种极其搞心态的情况:用户通过某些第三方渠道,安装了你上一版的旧包。而此时他的手机里,存的是你新版本创建的高版本数据库。
旧代码去读新数据库,一查表结构,发现版本号不对,直接抛异常。这种场景防不胜防。所以我们在初始化数据库时,除了看要不要升级,还得防一手降级。
typescript
const targetVersion = 2; // 当前代码期望的版本
async function initDb(context: Context) {
// 先获取已有数据库的版本
const store = await relationalStore.getRdbStore(context, {
name: 'MyApp.db',
securityLevel: relationalStore.SecurityLevel.S1
});
if (store.version > targetVersion) {
// 发生了降级安装!
console.warn('检测到数据库版本高于当前应用,准备清理重建');
// 没办法,为了保证应用能跑,只能把高版本的库删了重建空表
await store.close();
await relationalStore.deleteRdbStore(context, 'MyApp.db');
// 重新创建
return await createNewDatabase(context);
}
}
这里的关键语句是 store.version > targetVersion 的拦截。虽然这会导致新版本产生的数据丢失,但总好过应用无限闪退,连主页都进不去。这是兜底的防线,防的是那些非正常的安装路径。

大数据的批量插入优化
除了升级,平时迁移数据时,如果不注意批量操作,也会引发性能灾难。如果你在一个 for 循环里,每读出一条就 executeSql 插一条,一千条数据可能得跑好几秒,主线程直接卡死。
记住,批量数据必须包在事务里,并且尽可能使用参数化绑定的方式。
typescript
// 开启事务后,连续插入
store.beginTransaction();
for (let i = 0; i < dataList.length; i++) {
const item = dataList[i];
const valueBucket: relationalStore.ValuesBucket = {
'name': item.name,
'age': item.age
};
// 这时候执行 executeSql 或 insert,速度极快,因为没有频繁的磁盘 IO 握手
await store.insert('UserInfo', valueBucket);
}
store.commit(); // 一次性落盘
在事务包裹下,数据库不会频繁触发磁盘写入,所有的操作先记录在内存和日志中,直到 commit() 时才一次性刷入磁盘。这能把你处理海量数据的耗时缩短几十倍。
做本地存储开发,一定要有一颗敬畏之心。你的每一行 SQL 都决定着用户数据能不能活过下一次开屏。把事务焊死在代码里,把降级兜底做好,这就是数据守护者的基本素养。
更多推荐
所有评论(0)