26 数据库版本升级策略:从 v1 到 v2 的增量迁移实践

前言

在这里插入图片描述

图:26 数据库版本升级策略:从 v1 到 v2 的增量迁移实践 运行效果截图(HarmonyOS NEXT)

在应用迭代过程中,数据库表结构往往会发生变化——新增字段、创建新表、修改索引。如何在不丢失用户已有数据的前提下优雅地完成数据库迁移,是每个应用开发者必须掌握的技能。

本文以"鹿鹿·笔迹心理分析"项目的 [DatabaseService.ets](file:///Users/fiona/Downloads/bijixinli/harmony-app/entry/src/main/ets/features/data/DatabaseService.ets) 中从 v1 到 v2 的实战迁移为例,深入解析鸿蒙 RDB 的版本管理机制和增量升级策略。

鸿蒙官方·数据库版本管理:developer.huawei.com
项目源码仓库:harmony-app GitHub

数据库版本升级流程

图:鸿蒙 RDB 版本升级策略——增量迁移从 v1 到 vN

否,版本已是最新

是,需要升级

否,currentVersion == 2

应用启动 init

store.version < DB_VERSION?

直接使用 RdbStore

调用 upgrade 方法

currentVersion == 1?

执行 v1→v2 迁移脚本

ALTER TABLE / CREATE TABLE

store.version = 2

执行 v2→v3 脚本

还需继续升级?

一、版本管理的核心机制

1.1 RdbStore 的版本属性

在鸿蒙 RDB 中,每打开一个数据库,RdbStore 实例都会暴露一个 version 属性:

const currentVersion = store.version

各阶段的 version 值:

场景 store.version 值 说明
新创建的数据库 0 磁盘上还没有版本号
v1 版本的数据库 1 upgrade(from=0,to=1) 执行后 v1 = 1
v2 版本的数据库 2 upgrade(from=1,to=2) 执行后 v2 = 2
已更新过代码但未赋值的 仍为 1 需要显式 store.version = DB_VERSION

关键点store.version 读取的是数据库中记录的版本号,而非代码中声明的 DB_VERSION。正确做法是逐段升级后再赋值。

1.2 版本检测与升级入口

async init(context: common.UIAbilityContext): Promise<void> {
  this.store = await relationalStore.getRdbStore(context, DB_CONFIG)

  // 版本检测:store.version 从磁盘读取,DB_VERSION 是代码中的最新版
  await this.upgrade(this.store, this.store.version, DB_VERSION)

  // 写入最新版本号到数据库
  this.store.version = DB_VERSION
}

二、upgrade 方法的设计模式

2.1 逐段升级的结构

private async upgrade(
  store: relationalStore.RdbStore,
  fromVersion: number,
  toVersion: number
): Promise<void> {
  // 从 v0 到 v1:完整建表
  if (fromVersion < 1) {
    // 执行 v1 的 DDL
    // ...
  }

  // 从 v1 到 v2:新增字段
  if (fromVersion < 2) {
    // 执行 v2 的 DDL
    // ...
  }

  // 后续版本:追加 if (fromVersion < 3) { ... }
  //           if (fromVersion < 4) { ... }
}

增量升级的核心思想:每一个版本升级都是"添加"操作——在现有结构上增加新表或新列,确保所有老版本的用户都能平滑升级到最新版。

2.2 版本升级路径示例

// 场景 A:v0 → v2(新用户)
// 当前版本:0
// 执行:fromVersion < 1 → 建全部 5 张表
// 执行:fromVersion < 2 → 添加 3 个新列
// 最终版本:实际版本 2

// 场景 B:v1 → v2(老用户)
// 当前版本:1
// 跳过:fromVersion < 1 → 表已存在,不执行
// 执行:fromVersion < 2 → 添加 3 个新列
// 最终版本:2

// 场景 C:v0 → v1 → v2 → v3(跨越多个版本)
// 每个 `if (fromVersion < N)` 都会依次执行,确保不遗漏任何中间版本

三、v1 阶段:完整建表

3.1 DDL 语句

fromVersion < 1 分支负责初始版本的所有建表语句:

if (fromVersion < 1) {
  const ddls: string[] = [
    // 5 张表的 CREATE TABLE
    `CREATE TABLE IF NOT EXISTS user (...)` ,
    `CREATE TABLE IF NOT EXISTS archive (...)` ,
    `CREATE TABLE IF NOT EXISTS handwriting (...)` ,
    `CREATE TABLE IF NOT EXISTS report (...)` ,
    `CREATE TABLE IF NOT EXISTS relation (...)` ,

    // 索引
    `CREATE INDEX IF NOT EXISTS idx_handwriting_archive
       ON handwriting(archive_id, created_at DESC)`,
    `CREATE INDEX IF NOT EXISTS idx_report_handwriting
       ON report(handwriting_id)`
  ]

  for (const sql of ddls) {
    try {
      await store.executeSql(sql)
    } catch (e) {
      hilog.error(0x0000, 'DatabaseService', '建表 SQL 执行失败: %s', JSON.stringify(e))
      throw new Error('建表 SQL 执行失败: ' + JSON.stringify(e))
    }
  }
}

3.2 异常处理

关键做法:建表失败时立即抛出异常,阻止应用继续运行。因为数据库是应用的基础设施,表结构不完整时后续所有操作都会失败。尽早报错比在用户使用中途崩溃更好。

提示:CREATE TABLE IF NOT EXISTS 保证了幂等性——多次执行不会报错,也不会重复建表。

四、v2 阶段:字段新增

4.1 迁移需求

在 v2 版本中,需要为 handwriting 表增加 3 个字段:

  • device_type:设备类型(如 ‘Camera’, ‘Tablet’)
  • word_count:字数统计
  • write_duration:书写时长
if (fromVersion < 2) {
  const migrates: string[] = [
    'ALTER TABLE handwriting ADD COLUMN device_type TEXT DEFAULT \'\'',
    'ALTER TABLE handwriting ADD COLUMN word_count INTEGER DEFAULT 0',
    'ALTER TABLE handwriting ADD COLUMN write_duration INTEGER DEFAULT 0'
  ]

  for (const sql of migrates) {
    try {
      await store.executeSql(sql)
    } catch (e) {
      // 列已存在时静默忽略
      hilog.error(0x0000, 'DatabaseService', '迁移 SQL 执行失败: %s', JSON.stringify(e))
      // 不抛出异常:列已存在时可安全继续
    }
  }
}

4.2 异常处理的差异

阶段 异常处理 理由
v1 建表 throw new Error(...) 终止应用 核心结构缺失,无法正常运行
v2 迁移 日志记录,不抛出异常 列可能已存在(调试期反复创建),静默忽略

五、字段新增的限制与策略

5.1 SQLite ALTER TABLE 限制

SQLite 对 ALTER TABLE 的支持非常有限:

-- ✅ 支持
ALTER TABLE handwriting ADD COLUMN device_type TEXT DEFAULT ''

-- ❌ 不支持
ALTER TABLE handwriting DROP COLUMN device_type       -- SQLite < 3.35.0

-- ❌ 不支持
ALTER TABLE handwriting RENAME COLUMN device_type TO device    -- SQLite < 3.25.0

-- ❌ 不支持
ALTER TABLE handwriting ALTER COLUMN device_type SET NOT NULL

5.2 ALTER TABLE ADD COLUMN 的注意事项

// ✅ 正确:指定 DEFAULT 值
'ALTER TABLE handwriting ADD COLUMN device_type TEXT DEFAULT \'\''
'ALTER TABLE handwriting ADD COLUMN word_count INTEGER DEFAULT 0'
'ALTER TABLE handwriting ADD COLUMN write_duration INTEGER DEFAULT 0'

// ❌ 错误:NOT NULL 但无 DEFAULT
// (SQLite 要求:对已有表新增 NOT NULL 列,必须指定 DEFAULT)
'ALTER TABLE handwriting ADD COLUMN device_type TEXT NOT NULL'  // 会失败!

5.3 复杂的表结构变更

如果需要进行 SQLite 不直接支持的变更(如删除列、修改列类型),需要采用重建表的策略:

// 复杂变更策略(示例,非本项目)
if (fromVersion < 3) {
  await store.executeSql('BEGIN TRANSACTION')

  // 1. 创建新表
  await store.executeSql('CREATE TABLE handwriting_new (...)')

  // 2. 迁移数据
  await store.executeSql('INSERT INTO handwriting_new SELECT ... FROM handwriting')

  // 3. 删除旧表
  await store.executeSql('DROP TABLE handwriting')

  // 4. 重命名新表
  await store.executeSql('ALTER TABLE handwriting_new RENAME TO handwriting')

  // 5. 重建索引
  await store.executeSql('CREATE INDEX ...')

  await store.executeSql('COMMIT')
}

六、版本号管理规范

6.1 版本号的意义

// DatabaseService.ets 头部
const DB_VERSION = 2  // 每次表结构变化时 +1

// 版本号历史:
// v1 = 初始版本:5 张表 + 2 个索引
// v2 = handwriting 增加 device_type / word_count / write_duration

6.2 版本号更新流程

需求变更 → 需要修改表结构
  ↓
① 在 DatabaseService 中修改 DB_VERSION
   const DB_VERSION = 3
  ↓
② 在 upgrade() 方法末尾追加新分支
   if (fromVersion < 3) { ... }
  ↓
③ 更新注释说明版本变化
   // v3 = handwriting 新增 xxx 字段
  ↓
④ 更新 Models.ets 接口定义
   interface HandwritingEntity { xxx?: type }
  ↓
⑤ 更新对应 DAO 的 COLS 数组和 parseRow 方法

七、调试技巧

7.1 日志追踪

// DatabaseService.ets
hilog.info(0x0000, 'DatabaseService', 'RDB 初始化完成 v%d', DB_VERSION)

// upgrade 阶段
hilog.info(0x0000, 'DatabaseService', '建表完成 → v1')
// v1→v2
hilog.info(0x0000, 'DatabaseService', '迁移完成 → v2')

典型日志输出:

// 首次安装(v0 → v2)
[DatabaseService] 建表完成 → v1
[DatabaseService] 迁移完成 → v2
[DatabaseService] RDB 初始化完成 v2

// 老版本升级(v1 → v2)
[DatabaseService] 迁移完成 → v2
[DatabaseService] RDB 初始化完成 v2

// 已升级到 v2,再次启动(无变更)
[DatabaseService] RDB 初始化完成 v2

7.2 版本号重置(开发阶段)

开发调试时,可能需要重置数据库:

// 方案 A:删除数据库文件(需要在真机上清除应用数据)
// 设置 → 应用管理 → 鹿鹿 → 删除数据

// 方案 B:卸载重装
// adb uninstall com.example.lulu

// 方案 C:递增 DB_VERSION(不推荐在正式版使用)
const DB_VERSION = 999  // 所有已有数据会留在旧数据库中

八、向前兼容

8.1 v2 字段的兼容处理

由于 v2 新增的字段都有 DEFAULT 值,旧版本 DAO 的 COLS 数组也能正常查询:

// HandwritingDao.ets — COLS 数组包含新字段
const COLS: string[] = [
  'id', 'archive_id', 'source', 'image_path', 'ocr_text',
  'feature_json', 'energy_score',
  'device_type',    // ← v2 新增
  'word_count',     // ← v2 新增
  'write_duration',  // ← v2 新增
  'created_at'
]

8.2 增量升级的优势

特性 增量升级 全量重建
用户数据 ✅ 保留 ❌ 丢失
执行速度 ✅ 快(只需执行迁移 SQL) ❌ 慢(重建所有数据)
代码复杂度 ✅ 低(追加 if 分支) ❌ 高(需要备份恢复)
风险 ✅ 低 ❌ 高

总结

本文深入解析了鸿蒙 RDB 数据库版本升级的完整方案:

  1. 版本检测store.version 读取磁盘版本,与代码中的 DB_VERSION 对比
  2. 增量升级if (fromVersion < N) 逐段执行迁移代码
  3. v0→v1 建表:使用 CREATE TABLE IF NOT EXISTS + 索引
  4. v1→v2 迁移ALTER TABLE ADD COLUMN + DEFAULT 值
  5. 异常策略:建表失败终止应用,迁移失败静默忽略
  6. 向前兼容:新增字段默认值、DAO 包含所有列、Entity 接口可选中

核心原则:每个版本升级都是"添加"操作,不修改已有数据。用户从任何旧版本都能平滑升级到最新版。

下一篇文章将深入 RdbPredicates 条件查询——鸿蒙 ORM 风格的查询条件构建。

如果这篇文章对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,你的支持是我持续创作的动力!


参考资源:


你遇到过数据库升级问题吗?

  • A. 升级后字段缺失,数据读取崩溃
  • B. 版本号遗漏递增,升级逻辑未触发
  • C. 旧版本数据格式与新版本不兼容
  • D. 目前没有遇到过问题

欢迎在评论区分享你的数据库迁移经历!

下一篇文章将介绍 RdbPredicates 条件查询——如何用链式 API 构建精确的查询条件。

如果这篇文章对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,你的支持是我持续创作的动力!

Logo

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

更多推荐