HarmonyOS RDB 数据迁移实战:版本、表结构、回滚与校验
HarmonyOS RDB 数据迁移实战:版本、表结构、回滚与校验
应用升级最怕数据库结构变化。开发机上重新安装一切正常,老用户覆盖升级后却出现表不存在、字段为空、列表打不开。RDB 迁移的核心不是写几条 ALTER TABLE,而是知道当前库在哪个版本、应该逐级执行哪些脚本、执行失败后如何阻止脏数据继续运行。本文把 HarmonyOS 关系型数据库迁移拆成版本计划、迁移脚本、事务执行、结构校验和失败恢复。

本文会解决:
- 数据库版本如何和表结构绑定。
- 为什么迁移要逐级执行,不能只写最新版 SQL。
- 迁移后如何确认表和字段符合预期。
- 失败时如何记录证据并给用户稳定兜底。
1. 数据库迁移先定版本计划
关系型数据适合保存可查询、可更新、有结构的数据,例如草稿、路线记录、离线任务、订单缓存。每次表结构变化都应该对应一个版本号和一组迁移动作。

| 版本 | 变化 | 风险 |
|---|---|---|
| v1 | 创建基础表 | 首次安装 |
| v2 | 增加 status 字段 |
老数据默认值 |
| v3 | 增加索引 | 查询性能和重复索引 |
| v4 | 拆分扩展表 | 数据搬迁和回滚 |
版本计划要写在代码里,也要写在发布说明里。否则过几个月再看迁移脚本,很难判断哪条 SQL 对应哪个需求。
2. RDB 资料边界和工程目录
HarmonyOS 提供关系型数据库能力,适合结构化数据持久化。工程上建议把表结构、迁移执行和校验分开。
| 资料入口 | 工程落点 |
|---|---|
| 关系型数据库概述 | 选择 RDB 存储结构化数据 |
| RelationalStore API 参考 | 创建、执行 SQL、查询和事务能力 |
| 数据持久化方案选择 | 区分 RDB、Preferences、文件存储 |
建议目录:
entry/src/main/ets/
common/db/SchemaPlan.ets
common/db/MigrationRunner.ets
common/db/SchemaVerifier.ets
common/db/RollbackLog.ets
common/db/RouteRecordRepository.ets
业务仓库只关心数据读写,迁移模块负责把库升级到可用状态。
3. SchemaPlan 固定目标结构
目标结构要明确到表、字段、索引。不要只把 SQL 写在迁移里,否则无法判断最终结构是否正确。
export interface ColumnSpec {
name: string
type: 'INTEGER' | 'TEXT' | 'REAL' | 'BLOB'
required: boolean
}
export interface TableSpec {
name: string
columns: ColumnSpec[]
indexes: string[]
}
export const RouteTableSpec: TableSpec = {
name: 'route_record',
columns: [
{ name: 'id', type: 'TEXT', required: true },
{ name: 'title', type: 'TEXT', required: true },
{ name: 'distance', type: 'REAL', required: true },
{ name: 'status', type: 'INTEGER', required: true },
{ name: 'updated_at', type: 'INTEGER', required: true }
],
indexes: ['idx_route_updated_at', 'idx_route_status']
}
结构计划是迁移后的验收标准。脚本可以很多,但最终都要落到这个结构。
4. 迁移脚本要逐级执行
用户可能从 v1 升到 v4,也可能从 v3 升到 v4。迁移器必须根据当前版本逐级执行缺失脚本。
export interface MigrationStep {
from: number
to: number
statements: string[]
}
export const RouteMigrations: MigrationStep[] = [
{
from: 1,
to: 2,
statements: ['ALTER TABLE route_record ADD COLUMN status INTEGER DEFAULT 0']
},
{
from: 2,
to: 3,
statements: ['CREATE INDEX IF NOT EXISTS idx_route_updated_at ON route_record(updated_at)']
},
{
from: 3,
to: 4,
statements: ['CREATE INDEX IF NOT EXISTS idx_route_status ON route_record(status)']
}
]
逐级迁移的好处是路径清晰。不要写一个“如果没有字段就添加”的大杂烩,否则问题发生时很难还原用户升级路径。
5. MigrationRunner 执行脚本并记录版本
迁移执行要有目标版本、当前版本和失败记录。真实项目中应配合事务能力,确保部分 SQL 失败时不会留下半升级状态。
interface SqlExecutor {
execute(sql: string): Promise<void>
getVersion(): Promise<number>
setVersion(version: number): Promise<void>
}
export class MigrationRunner {
constructor(private readonly db: SqlExecutor) {}
async migrate(targetVersion: number): Promise<void> {
let current = await this.db.getVersion()
while (current < targetVersion) {
const step = RouteMigrations.find(item => item.from === current)
if (!step) {
throw new Error(`缺少迁移脚本:${current} -> ${current + 1}`)
}
for (const sql of step.statements) {
await this.db.execute(sql)
}
await this.db.setVersion(step.to)
current = step.to
}
}
}
这段代码把版本推进放在脚本执行之后。只有脚本执行成功,才更新版本号,避免下次启动误以为已经完成迁移。
6. SchemaVerifier 校验表结构
迁移完成后不能直接进入业务页。至少要确认关键表存在、关键字段存在、版本号正确。

export interface ExistingTableInfo {
tableName: string
columns: string[]
indexes: string[]
}
export class SchemaVerifier {
verify(spec: TableSpec, actual: ExistingTableInfo): string[] {
const errors: string[] = []
if (actual.tableName !== spec.name) {
errors.push(`表名不匹配:${actual.tableName}`)
}
for (const column of spec.columns) {
if (!actual.columns.includes(column.name)) {
errors.push(`缺少字段:${column.name}`)
}
}
for (const index of spec.indexes) {
if (!actual.indexes.includes(index)) {
errors.push(`缺少索引:${index}`)
}
}
return errors
}
}
结构校验不需要覆盖所有数据,但必须覆盖关键字段。否则业务页面第一次查询时才发现问题,用户体验会更差。
7. 失败恢复要阻止脏启动
迁移失败时,不要继续打开依赖数据库的页面。可以进入恢复页,提示用户稍后重试,同时记录失败版本、SQL 和错误信息。
export interface MigrationFailure {
fromVersion: number
targetVersion: number
failedSql?: string
message: string
occurredAt: number
}
export class RollbackLog {
private failures: MigrationFailure[] = []
append(failure: Omit<MigrationFailure, 'occurredAt'>): void {
this.failures.push({ ...failure, occurredAt: Date.now() })
}
latest(): MigrationFailure | undefined {
return this.failures[this.failures.length - 1]
}
}
失败记录是排查入口。没有它,线上用户只会看到“数据异常”,开发无法知道卡在哪个版本。
8. 仓库层只接收可用数据库
业务仓库不要自己判断迁移。应用启动时应先完成迁移和结构校验,再把可用数据库交给仓库层。
export interface RouteRecord {
id: string
title: string
distance: number
status: number
updatedAt: number
}
export class RouteRecordRepository {
constructor(private readonly db: SqlExecutor) {}
async save(record: RouteRecord): Promise<void> {
await this.db.execute(
`INSERT OR REPLACE INTO route_record(id,title,distance,status,updated_at)
VALUES('${record.id}','${record.title}',${record.distance},${record.status},${record.updatedAt})`
)
}
}
示例重点是分层:仓库层假设数据库已经可用,迁移问题不要扩散到每个业务方法。
9. RDB 迁移验收动作
| 场景 | 操作 | 预期结果 |
|---|---|---|
| 首次安装 | 新装应用 | 创建 v4 目标结构 |
| v1 覆盖升级 | 准备旧库后升级 | 逐级执行 v1 到 v4 |
| 缺少脚本 | 删除某个迁移步骤 | 启动时报明确错误 |
| 字段缺失 | 手动破坏表结构 | 校验失败并进入恢复页 |
| 数据保留 | 旧路线记录升级后查询 | 核心字段仍可读取 |
可以加一个迁移结果断言:
export function assertMigrationReady(version: number, errors: string[]): void {
if (version < 4) {
throw new Error(`数据库版本未升级到目标版本:${version}`)
}
if (errors.length > 0) {
throw new Error(`数据库结构异常:${errors.join(',')}`)
}
}
这个断言适合放在启动阶段,防止错误结构进入业务页面。
10. 数据迁移异常排查表
数据库迁移问题要优先拿到旧版本真实库样本。只看新装数据库没有意义,因为新装路径不会经过历史脚本。排查时建议记录当前库版本、目标版本、执行到哪条脚本、结构校验错误和业务页面入口,这些信息能快速判断是脚本缺失、SQL 失败还是仓库层误用。
| 现象 | 优先查看 | 处理建议 |
|---|---|---|
| 老用户升级崩溃 | 当前版本和迁移脚本 | 确认是否逐级执行 |
| 字段不存在 | 目标结构和实际结构 | 增加结构校验和补充脚本 |
| 数据丢失 | 迁移 SQL 和事务边界 | 避免先删后迁,保留备份路径 |
| 查询变慢 | 索引是否创建 | 关键查询字段要有索引计划 |
| 重装正常升级异常 | 旧库样本 | 用旧版本真实数据做覆盖升级验证 |
线上遇到迁移失败时,不要让应用继续进入依赖该表的页面。先展示恢复页或降级页,再把失败证据上报,这比让用户在多个业务页面连续报错更容易定位。
RDB 迁移复现场景:给读者一组可执行核验
数据库迁移要准备旧版本数据。空库迁移通过不代表真实用户升级安全,必须验证字段新增、索引变化和失败回滚。
| 核验维度 | 读者需要准备的证据 |
|---|---|
| 输入 | 页面入口、用户动作、关键参数 |
| 过程 | 日志、状态变化、异常分支 |
| 输出 | UI 表现、回调结果、持久化结果 |
| 回归 | 同场景重复执行后的结果 |
interface RdbReplayCase {
fromVersion: any
toVersion: any
rowCountBefore: any
rollbackReady: any
}
const replay80: RdbReplayCase = {
fromVersion: 'sample',
toVersion: 'sample',
rowCountBefore: 'sample',
rollbackReady: 'sample',
}
function assertReplay80(item: RdbReplayCase): void {
if (item.fromVersion >= item.toVersion) throw new Error('迁移版本方向错误')
}
这组核验要求带旧版本数据升级,适合验证 RDB 表结构变化和失败回滚是否可靠。
数据库升级回放表:把文章方法变成可复现动作
RDB 迁移必须带旧数据验证。建议准备一个旧版本库、一个空库、一个损坏库,分别验证迁移、初始化和回滚路径,避免只在开发库上通过。
| 回放动作 | 核验方式 |
|---|---|
| 旧库迁移成功 | 准备输入、执行操作、记录结果、给出结论 |
| 空库初始化成功 | 准备输入、执行操作、记录结果、给出结论 |
| 损坏库回滚 | 准备输入、执行操作、记录结果、给出结论 |
| 迁移日志可追溯 | 准备输入、执行操作、记录结果、给出结论 |
RDB 迁移文章要强调真实升级路径。读者不能只用新安装空库验证迁移,而要准备上一个正式版本的数据库文件,带着真实行数、旧字段和旧索引升级。升级失败时要能回滚,升级成功后要核对关键表行数和业务查询结果。这样数据库迁移才不会在用户升级后暴露问题。
11. 小结:数据库升级要可追、可验、可恢复
RDB 迁移不是 SQL 拼接任务,而是版本治理。目标结构要清晰,迁移脚本要逐级,执行过程要推进版本,完成后要校验关键字段,失败时要记录证据并阻止脏启动。这样应用升级时,老用户的数据不会成为最不可控的风险点。
更多推荐


所有评论(0)