【听见课堂 HarmonyOS NEXT 实战系列 14】HarmonyOS 数据库升级实战从 Schema v1 迁移到 v2第一次安装时建表很容易真正困难的是已经有用户数据后再改表。直接把CREATE TABLE增加两个字段只对新数据库有效旧设备上的表结构不会自动变化。如果应用读取不存在的列轻则页面报错重则用户无法进入应用。听见课堂当前把 RelationalStore schema 提升到 v2课程新增color_token任务新增due_at_ms并将旧任务的自然语言截止时间回填为时间戳。本文按照migrateAndSeed()和migrateV1ToV2()的真实实现拆解版本检测、事务、回填和测试矩阵。一、为什么需要 v2schema v1 已经能保存课程、字幕、板书和任务但后续出现两个新需求。1. 课程需要稳定的颜色语义如果页面只根据列表位置分配颜色课程排序变化后同一课程会变色。v2 在courses增加color_tokenTEXTNOTNULLDEFAULTocean_blue存的是主题 token而不是固定色值方便暗色和高对比模式在 UI 层映射。2. 任务中心需要真实时间排序v1 只有due_text例如“今晚”“明天 20:00 前”“本周五前”。每次打开页面重新解析会让“明天”不断向后移动也无法稳定判断是否过期。v2 增加due_at_msINTEGERNOTNULLDEFAULT0迁移时以同一个基准时刻解析旧文本之后排序、日期分组和过期判断都读取固定时间戳。二、版本号必须是代码常量和数据库记录的组合当前代码声明constSCHEMA_VERSION:number2;constMETA_SCHEMA_VERSION:stringschema_version;代码常量表示“当前应用能够理解的最高版本”schema_meta中的值表示“这个数据库已经迁移到哪个版本”。启动时比较二者才能决定首次建库、升级、正常打开还是拒绝降级读取。只修改常量不写迁移旧表不会变化只改表不更新元数据迁移可能每次启动重复执行。三、migrateAndSeed()的完整执行顺序当前初始化流程可以归纳为beginTransaction - 创建 schema_meta - 读取旧版本 - 版本过新则拒绝 - CREATE TABLE IF NOT EXISTS 当前结构 - version 1 时执行 v1 - v2 - 首次需要时写入种子 - 写入 schema_version 2 commit 任何异常 - rollBack - 初始化失败 - 上层切换内存降级对应的核心代码privateasyncmigrateAndSeed():Promisevoid{conststore:relationalStore.RdbStorethis.getStore();store.beginTransaction();try{awaitstore.executeSql(CREATE_SCHEMA_META_SQL);constversion:numberawaitthis.readSchemaVersion();if(versionSCHEMA_VERSION){thrownewError(Database schema is newer than this application.);}awaitstore.executeSql(CREATE_COURSES_SQL);awaitstore.executeSql(CREATE_TRANSCRIPT_SQL);awaitstore.executeSql(CREATE_SCANS_SQL);awaitstore.executeSql(CREATE_TASKS_SQL);if(version1){awaitthis.migrateV1ToV2();}// seed 与 meta 写入store.commit();}catch(error){store.rollBack();thrownewError(Failed to migrate classroom relational store.);}}版本号只在所有步骤成功后更新为 2避免迁移做到一半却被标记为完成。四、为什么先执行CREATE TABLE IF NOT EXISTS对全新数据库schema_meta中没有版本读取结果为 0。当前CREATE_*SQL 已经包含 v2 字段因此直接创建最新结构不需要从 v1 绕一圈。对 v1 数据库表已经存在CREATE TABLE IF NOT EXISTS不会改变旧表然后由migrateV1ToV2()增加字段。这形成两条路径数据库状态version动作全新安装0直接创建 v2 表并写种子已有 v11保留数据并执行 ALTER/回填已有 v22跳过迁移正常读取来自未来版本2拒绝打开避免旧应用破坏新结构五、v1 到 v2 的两个ALTER TABLE迁移方法先增加字段awaitstore.executeSql(ALTER TABLE courses ADD COLUMN color_token TEXT NOT NULL DEFAULT ocean_blue);awaitstore.executeSql(ALTER TABLE tasks ADD COLUMN due_at_ms INTEGER NOT NULL DEFAULT 0);两个字段都提供NOT NULL DEFAULT这样已有行会获得可读值。没有默认值时为含数据的旧表新增非空列往往会失败。color_token使用统一默认值可以保证页面立即可渲染后续若要按旧课程特征分配不同 token应另写确定性回填规则。六、为什么旧任务必须回填due_at_ms仅增加默认值 0 虽然能完成建表但所有旧任务都会变成“未指定日期”任务中心无法判断过期和本周分组。因此迁移读取旧 ID 与due_textconstresultSetawaitstore.querySql(SELECT id, due_text FROM tasks ORDER BY sort_order);consttaskIds:Arraystring[];constdueTexts:Arraystring[];try{while(resultSet.goToNextRow()){taskIds.push(this.getText(resultSet,id));dueTexts.push(this.getText(resultSet,due_text));}}finally{resultSet.close();}先把结果复制到普通数组并关闭 ResultSet再逐条更新资源边界更清晰。七、所有旧任务必须共享同一个迁移时刻迁移代码只调用一次Date.now()constmigrationNowMillis:numberDate.now();for(letindex:number0;indextaskIds.length;index){awaitthis.updateById(TABLE_TASKS,taskIds[index],{due_at_ms:TaskDateResolver.resolveDueText(dueTexts[index],migrationNowMillis)});}如果每条任务各取一次当前时间迁移跨过午夜时“今天”和“明天”可能落到不同基准日。统一基准时刻保证同批回填一致。八、自然语言回填不是无损转换TaskDateResolver当前支持“今晚/今天”“明天”“周一至周日”“已过期/昨天/上周”等有限表达不支持任意中文日期。因此可识别文本得到本地时间戳不可识别文本返回 0“本周五”在不同迁移日期可能落在过去或未来没有年份、时区和原始创建时间时语义无法完全还原。这不是迁移代码可以凭空解决的问题。更完善的 v1 设计应同时保存任务创建时间或原始解析基准当前文章必须把due_at_ms0视为有效降级而不是伪造日期。九、为什么要拒绝“数据库版本过新”用户可能先安装新版产生 schema v3之后回退到只支持 v2 的旧应用。旧代码不了解新字段、新约束和新状态继续写入可能破坏数据。当前保护是if(versionSCHEMA_VERSION){thrownewError(Database schema is newer than this application.);}异常向上传递后应用切到可见的内存降级。更理想的页面提示是“当前版本无法读取已有数据请升级应用”而不是自动删库。十、事务能保护什么不能保护什么事务目标是让建表、ALTER、回填、种子和版本更新作为一个整体提交。失败时执行rollBack()上层不应继续把数据库当作 v2 使用。但工程上仍需在目标 HarmonyOS 版本验证 DDL 在事务中的真实回滚行为不能只根据代码结构假设所有设备完全一致。尤其要测试第一个 ALTER 成功、第二个失败ALTER 成功、回填中断回填成功、写 meta 前进程结束再次启动是否能安全恢复。如果目标环境对部分 DDL 回滚有限制就需要增加“列是否存在”探测或分阶段迁移状态避免重复ADD COLUMN。十一、四类数据库状态必须分开测试1. 首次安装没有数据库文件和 meta。应直接得到 v2 表、默认课程颜色、任务时间戳和一次性种子。2. 真实 v1 升级先用 v1 schema 创建数据库并写入自定义课程、字幕、板书和任务再安装 v2。验证原记录数量、正文和确认状态不变新字段完成回填。3. 空库表存在但没有业务数据。迁移不能因为查询结果为空而失败也不能在用户明确清空后每次启动重新注入种子。4. 脏数据至少覆盖空截止文本、无法解析日期、重复 ID、缺少 meta、版本过新和部分字段异常。结果可以降级或阻断但不能静默删除用户内容。十二、迁移后的验证不能只查版本号schema_version2只是一个信号。迁移完成后还应检查courses.color_token和tasks.due_at_ms列存在旧课程、字幕、扫描和任务行数保持已确认/已完成状态未改变可识别的截止文本得到合理时间戳不可识别文本保持due_at_ms0并显示“未指定”P09 的排序、过期和日历分组符合预期删除课程和清空全部数据事务仍有效强停重启后仍读取 v2不重复迁移。可以把验证拆成结构、数据、业务三层而不是只执行一条SELECT schema_version。十三、上下文文档与源码版本漂移怎么处理项目早期架构文档可能仍写“当前 schema version 为 1”而当前源码常量已经是 2。写技术文章或交付报告时应明确采用当前源码和本轮验证结果历史文档只作为当时基线。正确做法是同步更新上下文或记录漂移不应为了让材料一致而把源码事实写回 v1。可审计项目允许历史存在但必须标注时间和适用版本。十四、后续 v3 应怎样扩展当 v3 到来时不建议继续堆一个大函数。可以按版本逐级迁移letversion:numberawaitreadSchemaVersion();if(version2){awaitmigrateV1ToV2();version2;}if(version3){awaitmigrateV2ToV3();version3;}每一步只理解相邻版本配套独立夹具和后置条件。版本更新仍要在该步成功后发生。若数据量增大还要考虑批量更新、进度提示、超时和可恢复中断。十五、迁移验收清单当前 schema 常量与目标结构一致全新安装直接创建最新结构v1 数据库通过 ALTER 和回填升级新增非空列有合理默认值所有旧任务共享同一迁移基准时刻ResultSet 在finally中关闭版本过新时拒绝写入而不是删库迁移失败切换为可见降级首装、升级、空库、脏数据分别测试验证字段、数据和页面业务而不只检查版本号没有把“代码存在迁移”写成“目标设备迁移已验证”。十六、总结数据库升级的本质是保护已有用户事实。听见课堂的 v1→v2 迁移在一个初始化事务中完成版本检测、字段新增、截止时间回填、种子控制和版本写入并对未来版本设置拒绝保护。这套实现已经具备清晰主链但仍需要用真实 v1 数据库夹具验证 DDL 中断、脏数据和设备差异。只有迁移脚本存在、目标环境执行通过、迁移后业务页面正确三者同时成立才能把数据库升级标记为passed。