26 数据库版本升级策略:从 v1 到 v2 的增量迁移实践
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
一、版本管理的核心机制
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 数据库版本升级的完整方案:
- 版本检测:
store.version读取磁盘版本,与代码中的DB_VERSION对比 - 增量升级:
if (fromVersion < N)逐段执行迁移代码 - v0→v1 建表:使用
CREATE TABLE IF NOT EXISTS+ 索引 - v1→v2 迁移:
ALTER TABLE ADD COLUMN+ DEFAULT 值 - 异常策略:建表失败终止应用,迁移失败静默忽略
- 向前兼容:新增字段默认值、DAO 包含所有列、Entity 接口可选中
核心原则:每个版本升级都是"添加"操作,不修改已有数据。用户从任何旧版本都能平滑升级到最新版。
下一篇文章将深入 RdbPredicates 条件查询——鸿蒙 ORM 风格的查询条件构建。
如果这篇文章对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,你的支持是我持续创作的动力!
参考资源:
- relationalStore 版本管理
- SQLite ALTER TABLE
- [DatabaseService 项目源码](file:///Users/fiona/Downloads/bijixinli/harmony-app/entry/src/main/ets/features/data/DatabaseService.ets)
- CREATE TABLE IF NOT EXISTS
- 鸿蒙数据持久化指南
- RdbStore 执行 SQL
- 鸿蒙开发文档
- [HandwritingDao 项目源码](file:///Users/fiona/Downloads/bijixinli/harmony-app/entry/src/main/ets/features/data/HandwritingDao.ets)
- [Models.ets 实体定义](file:///Users/fiona/Downloads/bijixinli/harmony-app/entry/src/main/ets/features/data/Models.ets)
你遇到过数据库升级问题吗?
- A. 升级后字段缺失,数据读取崩溃
- B. 版本号遗漏递增,升级逻辑未触发
- C. 旧版本数据格式与新版本不兼容
- D. 目前没有遇到过问题
欢迎在评论区分享你的数据库迁移经历!
下一篇文章将介绍 RdbPredicates 条件查询——如何用链式 API 构建精确的查询条件。
如果这篇文章对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,你的支持是我持续创作的动力!
更多推荐

所有评论(0)