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属性:
constcurrentVersion=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 版本检测与升级入口
asyncinit(context:common.UIAbilityContext):Promise<void>{this.store=awaitrelationalStore.getRdbStore(context,DB_CONFIG)// 版本检测:store.version 从磁盘读取,DB_VERSION 是代码中的最新版awaitthis.upgrade(this.store,this.store.version,DB_VERSION)// 写入最新版本号到数据库this.store.version=DB_VERSION}二、upgrade 方法的设计模式
2.1 逐段升级的结构
privateasyncupgrade(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){constddls: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(constsqlofddls){try{awaitstore.executeSql(sql)}catch(e){hilog.error(0x0000,'DatabaseService','建表 SQL 执行失败: %s',JSON.stringify(e))thrownewError('建表 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){constmigrates: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(constsqlofmigrates){try{awaitstore.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的支持非常有限:
-- ✅ 支持ALTERTABLEhandwritingADDCOLUMNdevice_typeTEXTDEFAULT''-- ❌ 不支持ALTERTABLEhandwritingDROPCOLUMNdevice_type-- SQLite < 3.35.0-- ❌ 不支持ALTERTABLEhandwritingRENAMECOLUMNdevice_typeTOdevice-- SQLite < 3.25.0-- ❌ 不支持ALTERTABLEhandwritingALTERCOLUMNdevice_typeSETNOTNULL5.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){awaitstore.executeSql('BEGIN TRANSACTION')// 1. 创建新表awaitstore.executeSql('CREATE TABLE handwriting_new (...)')// 2. 迁移数据awaitstore.executeSql('INSERT INTO handwriting_new SELECT ... FROM handwriting')// 3. 删除旧表awaitstore.executeSql('DROP TABLE handwriting')// 4. 重命名新表awaitstore.executeSql('ALTER TABLE handwriting_new RENAME TO handwriting')// 5. 重建索引awaitstore.executeSql('CREATE INDEX ...')awaitstore.executeSql('COMMIT')}六、版本号管理规范
6.1 版本号的意义
// DatabaseService.ets 头部constDB_VERSION=2// 每次表结构变化时 +1// 版本号历史:// v1 = 初始版本:5 张表 + 2 个索引// v2 = handwriting 增加 device_type / word_count / write_duration6.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.etshilog.info(0x0000,'DatabaseService','RDB 初始化完成 v%d',DB_VERSION)// upgrade 阶段hilog.info(0x0000,'DatabaseService','建表完成 → v1')// v1→v2hilog.info(0x0000,'DatabaseService','迁移完成 → v2')典型日志输出:
// 首次安装(v0 → v2) [DatabaseService] 建表完成 → v1 [DatabaseService] 迁移完成 → v2 [DatabaseService] RDB 初始化完成 v2 // 老版本升级(v1 → v2) [DatabaseService] 迁移完成 → v2 [DatabaseService] RDB 初始化完成 v2 // 已升级到 v2,再次启动(无变更) [DatabaseService] RDB 初始化完成 v27.2 版本号重置(开发阶段)
开发调试时,可能需要重置数据库:
// 方案 A:删除数据库文件(需要在真机上清除应用数据)// 设置 → 应用管理 → 鹿鹿 → 删除数据// 方案 B:卸载重装// adb uninstall com.example.lulu// 方案 C:递增 DB_VERSION(不推荐在正式版使用)constDB_VERSION=999// 所有已有数据会留在旧数据库中八、向前兼容
8.1 v2 字段的兼容处理
由于 v2 新增的字段都有DEFAULT值,旧版本 DAO 的 COLS 数组也能正常查询:
// HandwritingDao.ets — COLS 数组包含新字段constCOLS: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 构建精确的查询条件。
如果这篇文章对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,你的支持是我持续创作的动力!