ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

鸿蒙数据库高级迁移与版本兼容:Schema版本管理/增量迁移脚本/向前向后兼容/无感知升级方案

鸿蒙数据库高级迁移与版本兼容:Schema版本管理/增量迁移脚本/向前向后兼容/无感知升级方案 一、前置思考1.1 数据库升级是最容易翻车的发布App 迭代必然改表加字段、改字段类型、拆表、合表。数据库升级翻车案例比比皆是翻车1: v1 直接 ALTER TABLE 加 NOT NULL 字段, 老用户升级后所有历史行报错 翻车2: 升级脚本顺序写错, v2→v3 的脚本依赖 v3 的字段 翻车3: 升级到一半崩溃, 数据库损坏, 用户数据全丢 翻车4: 老版本 App 打开新版本数据库 → 直接崩溃 (向后兼容缺失)1.2 版本管理的本质数据库迁移的本质是在不同 Schema 版本之间无损失、可回滚、并发安全地转换数据。核心挑战挑战说明版本演进每个版本对应一个 Schema 快照增量迁移vN → vN1 只跑增量脚本向前兼容新版本 App 能读老数据向后兼容老版本 App 能读新库(降级)无感知升级不阻塞、不丢数据1.3 本文路线给出版本登记表、增量迁移脚本框架、兼容性策略、无感知升级与回滚完整方案。二、核心原理2.1 Schema 版本演进模型v1: CREATE TABLE user(id, name) │ 迁移脚本 M1: ALTER TABLE user ADD COLUMN age INTEGER ▼ v2: CREATE TABLE user(id, name, age) │ 迁移脚本 M2: CREATE TABLE order(...) ▼ v3: CREATE TABLE user(id, name, age) order 表关键设计数据库文件里持久化版本号PRAGMA user_version或自建 meta 表启动时对比目标版本与当前版本顺序执行中间缺失的迁移脚本。2.2 增量迁移脚本框架迁移脚本 有序数组, 下标即目标版本 MIGRATIONS [ { to: 2, sql: ALTER TABLE user ADD COLUMN age INTEGER DEFAULT 0 }, { to: 3, sql: CREATE TABLE IF NOT EXISTS order(...) }, { to: 4, sql: ALTER TABLE user ADD COLUMN vip INTEGER DEFAULT 0 }, ] 升级流程: current 1 (库内版本) target 4 (代码版本) for v in (current1 .. target): 执行 MIGRATIONS[v-2].sql (按序) 最后更新库内版本 target2.3 为什么不能只做从1升到N用户可能从 v1、v2、v3 任意版本升级老版本一直没更新的用户只写最终形态脚本中间版本用户无法迁移增量脚本必须可重放、幂等IF NOT EXISTS / 防御式写法。2.4 向前/向后兼容向前兼容 (新 App 读旧库): 新 App 启动 → 检测到库版本旧 → 自动执行迁移 → 升级到新版本 → 必须保证迁移脚本正确, 否则老用户升级后崩溃 向后兼容 (旧 App 读新库): 用户降级/回滚到旧 App → 旧代码不认识新字段 → 防御: 新字段带 DEFAULT / 可空; 查询用列名而非 SELECT * → 理想: 新旧版本共享同一份兼容 Schema 子集三、源码/API 深度解析3.1 鸿蒙 RelationalStore 版本迁移import{relationalStore}fromkit.ArkData;import{common}fromkit.AbilityKit;interfaceMigration{toVersion:number;sqls:string[];}// 版本迁移表 (有序)constMIGRATIONS:Migration[][{toVersion:2,sqls:[ALTER TABLE user ADD COLUMN age INTEGER DEFAULT 0]},{toVersion:3,sqls:[CREATE TABLE IF NOT EXISTS order(id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER, amount REAL)]},{toVersion:4,sqls:[ALTER TABLE user ADD COLUMN vip INTEGER DEFAULT 0,CREATE INDEX IF NOT EXISTS idx_user_vip ON user(vip)]}];constCURRENT_VERSION4;asyncfunctionopenWithMigration(context:common.Context):PromiserelationalStore.RdbStore{constconfig:relationalStore.StoreConfig{name:app.db,securityLevel:relationalStore.SecurityLevel.S1,// 关键: 声明当前版本号, 系统在打开时触发版本变化};constrdbawaitrelationalStore.getRdbStore(context,config);awaitrdb.executeSql(PRAGMA user_version 1);// 首次建库标记版本// 手动迁移框架 (示例: 通过 meta 表记录版本)constcurrentawaitgetDbVersion(rdb);for(constmofMIGRATIONS){if(m.toVersioncurrentm.toVersionCURRENT_VERSION){rdb.beginTransaction();try{for(constsqlofm.sqls){awaitrdb.executeSql(sql);}awaitsetDbVersion(rdb,m.toVersion);// 每个增量单独提交, 防半途崩溃rdb.commit();}catch(e){rdb.rollBack();throwe;}}}returnrdb;}asyncfunctiongetDbVersion(rdb:relationalStore.RdbStore):Promisenumber{constrsawaitrdb.querySql(PRAGMA user_version);letv1;if(rs.goToFirstRow()){vrs.getLong(0);}rs.close();returnv;}asyncfunctionsetDbVersion(rdb:relationalStore.RdbStore,v:number):Promisevoid{awaitrdb.executeSql(PRAGMA user_version ${v});}3.2 防御式迁移写法// 幂等: 重复执行不报错constSAFE_SQLS[ALTER TABLE user ADD COLUMN age INTEGER DEFAULT 0,// 若列已存在会报错// 防御写法: 先查列是否存在];asyncfunctionaddColumnIfNotExists(rdb:relationalStore.RdbStore,table:string,column:string,ddl:string):Promisevoid{constrsawaitrdb.querySql(PRAGMA table_info(${table}));letexistsfalse;while(rs.goToNextRow()){if(rs.getString(rs.getColumnIndex(name))column){existstrue;break;}}rs.close();if(!exists){awaitrdb.executeSql(ddl);}}3.3 无感知升级启动时 后台// 策略: 冷启动时快速迁移小表; 大表数据改造放后台 TaskPoolasyncfunctionupgradeSmart(context:common.Context):Promisevoid{constrdbawaitrelationalStore.getRdbStore(context,{name:app.db,securityLevel:relationalStore.SecurityLevel.S1});// 阶段1: 快速 Schema 迁移 (毫秒级, 主线程可接受)// 阶段2: 数据搬运/清洗 (秒级, 后台线程)// 阶段3: 版本号更新 标记完成awaitmigrateSchema(rdb);// DDL 为主startBackgroundDataTransform();// 异步数据改造}四、企业级实战落地4.1 迁移流水线架构发布流程: 代码升级 MIGRATIONS 数组 → 灰度发布 → 老用户启动触发迁移 → 迁移失败自动回滚上一版本 → 上报错误 → 修复后重发 启动流程: open 数据库 ├─ 版本 目标 → 正常使用 ├─ 版本 目标 → 顺序执行增量迁移 (每步单独事务) │ 失败 → 回滚到迁移前版本 → 保留现场 └─ 版本 目标 (降级) → 只读兼容模式 或 提示更新4.2 数据改造迁移非 DDL// 示例: v3→v4 把 user 表拆出 profile 表asyncfunctionmigrateSplitTable(rdb:relationalStore.RdbStore):Promisevoid{// 1. 建新表awaitrdb.executeSql(CREATE TABLE IF NOT EXISTS user_profile(user_id INTEGER PRIMARY KEY, avatar TEXT, bio TEXT));// 2. 旧数据搬迁 (分批避免锁长事务)constrsawaitrdb.querySql(SELECT id, avatar, bio FROM user);constbatch:relationalStore.ValuesBucket[][];while(rs.goToNextRow()){batch.push({user_id:rs.getLong(rs.getColumnIndex(id)),avatar:rs.getString(rs.getColumnIndex(avatar)),bio:rs.getString(rs.getColumnIndex(bio))}asrelationalStore.ValuesBucket);if(batch.length500){awaitrdb.batchInsert(user_profile,batch);batch.length0;}}if(batch.length0){awaitrdb.batchInsert(user_profile,batch);}rs.close();}4.3 向后兼容防御// 旧版本 App 读新库: 避免 SELECT * 依赖列顺序// 正确: 显式列名 容错处理constrsawaitrdb.querySql(SELECT id, name, age FROM user);// 新字段 age 在旧 App 中不存在 → 旧 App 不会 select age, 不受影响// 但旧 App 的 INSERT 不带新字段 → 新字段必须有 DEFAULT// 新表创建用 IF NOT EXISTS, 列定义全部带默认值4.4 迁移监控与回滚阶段监控点失败动作Schema 迁移每步耗时/失败率回滚上一步 上报数据改造进度/一致性断点续做 (幂等)版本更新成功/失败失败不改版本号升级后验证关键查询回归异常 → 修复重发五、问题排查与性能优化坑现象原因解决老用户升级崩溃打开就崩迁移脚本有 bug单步事务 回滚NOT NULL 报错加列失败老数据无值带 DEFAULT升级一半中断库版本错乱多步无事务每步单独提交重复执行报错迁移重放失败脚本不幂等IF NOT EXISTS大表迁移卡死升级卡 10 秒长事务锁分批迁移降级崩溃旧 App 打不开新字段无默认值全字段 DEFAULT数据丢失迁移后少了搬迁逻辑漏迁移前后 count 校验5.1 迁移前备份// 大版本迁移前备份数据库文件, 失败可恢复import{fileIoasfs}fromkit.CoreFileKit;functionbackupDb(dbPath:string):string{constbakPathdbPath.bak_Date.now();fs.copyFileSync(dbPath,bakPath);returnbakPath;// 迁移失败时用备份恢复}5.2 灰度与开关功能开关 (Feature Flag): 新迁移代码默认关闭 → 白名单用户开启 → 验证稳定后全量 失败 → 关开关 → 用户走旧路径5.3 版本兼容矩阵测试场景测试v1 → v4老老用户直升v2 → v4中间版本v3 → v4相邻版本v4 → v4同版本重开v5 库 v4 代码降级兼容迁移中断恢复模拟崩溃重试六、高阶总结与最佳实践版本登记是地基数据库文件持久化版本号代码维护有序增量脚本数组。增量而非全量每个版本一个脚本从任意旧版本都能顺迁移。每步一个事务单步失败只回滚当前步不破坏已完成的迁移。防御式写法IF NOT EXISTS、DEFAULT 值、列存在性检查保证幂等。无感知升级DDL 前台快迁、数据改造后台分批、失败自动回滚监控上报。一句话记住版本号登记在库、增量脚本有序重放、每步独立事务、字段全部带默认值、大表分批后台迁——升级无感知失败能回滚。
返回列表