开发者技术支持-鸿蒙应用数据库升级(Schema Change)导致数据丢失的完整解决方案

hm688c71e43a460
发布于 2025-9-22 11:05
浏览
0收藏

在鸿蒙应用持续迭代的过程中,业务需求变化常常伴随着数据库表结构的变更(如增加新字段、修改字段类型等)。如果处理不当,特别是在跨版本升级时,极易引发数据丢失、应用崩溃等严重生产问题。本文将深入分析这一问题,并提供一套完整的解决方案。

1. 问题说明

问题场景:

某鸿蒙应用在发布V2.0版本时,为了新增一个功能,需要在原有的用户信息表 ​​user​​ 中增加一个 ​​nickname​​(昵称)字段。开发者在本地测试后正常发布。

生产问题:

部分已安装V1.0版本的老用户升级到V2.0后,启动应用时出现闪退。更严重的是,成功启动应用后,部分用户发现原有的账户数据(如收藏列表、浏览记录)全部丢失,变成了一个新用户的状态。


2. 原因分析

该问题的根源在于数据库版本升级策略的缺失或错误实现

  1. 默认行为误区:鸿蒙的 RDB 商店在打开数据库时,如果指定的版本号 (version) 比当前数据库的版本号高,会自动触发 upgrade 操作。然而,如果没有为upgrade​显式地编写迁移逻辑,数据库框架会默认采用“删除旧表,创建新表”的策略。这就是导致用户数据被清空的直接原因。
  2. 错误的升级操作:开发者可能在 ​​onUpgrade​​ 回调中只写了创建新表的SQL(​​CREATE TABLE IF NOT EXISTS ...​​),而没有处理旧数据的迁移。例如:

onUpgrade: (db, oldVersion, newVersion) => {
    // 只有创建新表的语句,没有数据迁移逻辑
    db.executeSql("CREATE TABLE IF NOT EXISTS user (id INTEGER PRIMARY KEY, name TEXT, nickname TEXT)");
    // 此时,老表已经被丢弃,其中的数据全部丢失!
}
  1. 版本管理混乱:应用有多个历史版本,每个版本可能都有不同的数据库结构。升级路径可能不是从 v1 直接到 v2,也可能是从 v1 到 v3,或者从 v2 回退到 v1。升级脚本如果没有考虑所有可能的升级路径,就会出错。

3. 解决思路

解决此问题的核心思路是:在数据库结构变更时,必须编写安全的数据迁移脚本,将旧数据库中的数据无损地迁移到新结构的数据库中。

  1. 预案与回滚
  • 立即下线有问题的V2.0版本,推送一个修复前的V1.1版本(版本号高于V1.0但低于V2.0),优先阻止问题影响更多用户。
  • 如果用户数据在客户端有加密或备份,尝试引导用户进行数据恢复。
  1. 彻底修复方案
  • 规范版本管理:为数据库定义严格的版本号,每次表结构变更,版本号必须+1。
  • 实现增量升级:在 onUpgrade 方法中,根据 oldVersion 和 newVersion 的差异,编写一步一步的升级脚本,确保从任何一个历史版本升级到最新版本都能正确执行。
  • 使用ALTER TABLE语句:优先使用 ALTER TABLE ... ADD COLUMN 等标准SQL语句来新增字段,避免重建表。
  • 复杂变更分步处理:对于无法用 ALTER 语句完成的变更(如修改字段类型、删除字段),需要创建临时表,将旧表数据导入临时表,删除旧表,再创建新表,最后将数据从临时表导回新表。

4. 解决方案

以下是在鸿蒙应用中正确实现数据库升级的代码示例。

1. 定义数据库版本常量

export const DB_CONFIG = {
    NAME: 'myApp.db',
    VERSION_1: 1, // 初始版本
    VERSION_2: 2, // 新增nickname字段
    CURRENT_VERSION: 2
};

2. 正确的onUpgrade实现

import { relationalStore } from '@ohos.data.relationalStore';
import { DB_CONFIG } from './DatabaseUtils';

const STORE_CONFIG = {
    name: DB_CONFIG.NAME,
    securityLevel: relationalStore.SecurityLevel.S1
};

// 获取RDB连接
relationalStore.getRdbStore(this.context, STORE_CONFIG, (err, db) => {
    if (err) {
        console.error(`Failed to get RdbStore. Code:${err.code}, message:${err.message}`);
        return;
    }
    console.info('Succeeded in getting RdbStore.');

    // 版本变化时,会触发Upgrade
    db.version = DB_CONFIG.CURRENT_VERSION;

    db.on('versionChange', (event) => {
        // 版本变更事件,通常在这里不处理具体逻辑,但可以监听
        console.info(`Version changed from ${event.oldVersion} to ${event.newVersion}`);
    });

    // 核心:数据库升级处理
    db.upgrade((db, oldVersion, newVersion, callback) => {
        console.info(`Starting database upgrade from v${oldVersion} to v${newVersion}`);
        /* 使用事务保证升级操作的原子性 */
        db.beginTransaction()
            .then(() => {
                // 根据旧版本号,逐步升级
                if (oldVersion < 2) {
                    // 从v1升级到v2:增加nickname字段
                    console.info('Upgrading database from v1 to v2: ADD COLUMN nickname');
                    return db.executeSql("ALTER TABLE user ADD COLUMN nickname TEXT");
                }
                // 未来如果升级到v3,可以在这里添加 else if (oldVersion < 3) {...}
            })
            .then(() => {
                db.commit(); // 提交事务
                callback(); // 升级成功,调用callback
                console.info('Database upgrade completed successfully.');
            })
            .catch((err) => {
                db.rollback(); // 升级失败,回滚事务
                console.error(`Database upgrade failed. Code:${err.code}, message:${err.message}`);
                // 回调失败,数据库打开会失败,避免用户使用一个有问题的数据库
                throw err;
            });
    });
});

3. 复杂变更的示例(需要重建表)

假设V3版本需要将表 ​​user​​ 中的 ​​name​​ 字段拆分为 ​​first_name​​ 和 ​​last_name​​,无法用 ​​ALTER​​ 完成。

分类
标签
收藏
回复
举报
回复
    相关推荐