数据库升了个版本,用户的本地记录全清零了
应用迭代,给本地 RDB 数据库加个字段或者新建个表再正常不过。我见过一个极其惨烈的线上事故,某个版本的更新之后,大批用户反馈“我的收藏全没了”、“笔记全清空了”。
排查下来,发现开发同学在 onUpgrade 里为了图省事,直接写了 DROP TABLE,然后重新 CREATE TABLE。测试阶段因为都是干净的安装包,没人发现问题;一上线,存量用户一升级,数据直接灰飞烟灭。
关系型数据库(RDB)的版本管理,不是简单地改个 version 数字,它涉及存量数据的无损迁移。搞不好,就是生产事故。
识别 onUpgrade 的触发机制
在鸿蒙里操作 RDB,通常用到 relationalStore。初始化数据库时,你需要传入一个 StoreConfig,里面有个 version 属性。
typescript
import { relationalStore } from '@kit.ArkData';
const STORE_CONFIG: relationalStore.StoreConfig = {
name: 'NoteBook.db',
securityLevel: relationalStore.SecurityLevel.S1,
version: 2 // 从 1 升到了 2
};
// 模拟初始化
async function initDb(context: Context) {
const store = await relationalStore.getRdbStore(context, STORE_CONFIG);
// 如果 store.version 从 1 变成了 2,系统会自动尝试触发升级逻辑
if (store.version === 2) {
await migrateV1ToV2(store);
}
}
关键点在于 version。当用户的设备上已经存在 NoteBook.db,且版本是 1。你发版将代码里的 version 改成 2。用户覆盖安装打开应用时,getRdbStore 会发现版本不一致。
但这里有一个鸿蒙独有的坑:如果你在 StoreConfig 里配置了 version,系统会自动将数据库的版本号更新为你指定的值,但这只是改了版本号,它不会自动帮你执行表结构变更。如果你没有任何迁移代码,直接去查询新加的字段,立刻就会报 no such column 的错误。
用事务保证迁移的原子性
数据迁移最怕的是什么?迁移到一半,应用崩溃了,或者手机没电了。如果迁移逻辑没有事务保护,用户的数据库就会变成一个半新半旧的“缝合怪”,既不能用旧逻辑查,也不能用新逻辑查。
正确的做法,是把所有的表结构修改和数据搬运,全部包在 RDB 的事务里。要么全成功,要么全回滚。
typescript
async function migrateV1ToV2(store: relationalStore.RdbStore): Promise<void> {
// 开启事务
store.beginTransaction();
try {
// 1. 创建新表
await store.executeSql(
'CREATE TABLE IF NOT EXISTS NoteV2 (id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT, content TEXT, tags TEXT)'
);
// 2. 把旧表数据搬过来
await store.executeSql(
'INSERT INTO NoteV2 (title, content) SELECT title, content FROM Note'
);
// 3. 删除旧表
await store.executeSql('DROP TABLE Note');
// 4. 重命名新表
await store.executeSql('ALTER TABLE NoteV2 RENAME TO Note');
// 提交事务
store.commit();
} catch (e) {
// 发生异常,回滚所有操作,保证旧数据完好无损
store.rollBack();
console.error(`迁移失败,数据已回滚: ${e.message}`);
}
}
这段代码是这次升级的核心。beginTransaction 开启事务后,所有 executeSql 的变更都在内存和日志中记录。只有走到 store.commit(),系统才会真正落盘。一旦中间出现异常被 catch 捕获,调用 store.rollBack(),数据库就会瞬间回到升级前的状态,用户的数据绝不会丢。
常见错误是没写 rollBack(),或者把 commit() 写在了 try 块的中间,导致后面的 SQL 如果失败,前面已经提交的部分无法撤销。
降级与数据备份兜底
除了升级,还有一种极其隐蔽的崩溃场景:降级。有些用户可能通过三方渠道安装了你之前的旧版本。如果旧版本代码读取到了高版本留下的数据库(因为旧版本代码里没有新表结构),一跑就崩。
对于这种情况,在初始化数据库时,一定要判断版本号。
typescript
if (store.version > STORE_CONFIG.version) {
// 说明发生了降级,只能把旧库删掉,重建一个空库兜底
// 虽然丢数据,但总比一直崩溃强
await relationalStore.deleteRdbStore(context, 'NoteBook.db');
store = await relationalStore.getRdbStore(context, STORE_CONFIG);
}
同时,在执行高危迁移之前,最稳妥的方式是做一次文件级的备份。把 NoteBook.db 复制到一个临时目录,如果迁移彻底失败,还可以尝试引导用户恢复。
做本地存储开发,一定要有一个共识:用户的数据无价。不要相信“测试没问题线上就没问题”,存量数据往往包含了各种奇奇怪怪的脏数据。用 try-catch 包好你的迁移逻辑,用事务守住最后一道底线,这才是合格的数据守卫者。
更多推荐
所有评论(0)