鸿蒙应用开发实战【40】— 数据库版本升级ALTER TABLE策略

本文是「号码助手全栈开发系列」第 40 篇,持续更新中…
开源社区:https://openharmonycrossplatform.csdn.net


前言

数据库从第 1 版迭代到第 N 版,表结构不可避免要演进——新增字段、修改默认值、添加索引甚至拆分表。HarmonyOS RDB 通过 version + onUpgrade 回调来管理版本迁移。

本篇涵盖:版本号管理机制、ALTER TABLE ADD COLUMN 语法、onUpgrade 升级流程、新增字段默认值策略、索引增量创建、数据迁移兼容性、降级防护、升级前后的完整性校验。

在这里插入图片描述


一、RDB 版本号管理机制

1.1 StoreConfig 中的版本

HarmonyOS 的 relationalStore.StoreConfig 没有直接传入版本号的字段,版本由 RdbStore.version 属性管理:

import relationalStore from '@ohos.data.relationalStore';

const DB_CONFIG: relationalStore.StoreConfig = {
  name: 'number_assistant.db',
  securityLevel: relationalStore.SecurityLevel.S2
};

// 初始化后获取版本
const store = await relationalStore.getRdbStore(context, DB_CONFIG);
console.info('当前数据库版本:', store.version);

1.2 版本升级流程

store.version 在首次创建时为 0。我们在 init() 中手动管理版本:

async init(context: UIAbilityContext): Promise<void> {
  this.store = await relationalStore.getRdbStore(context, DB_CONFIG);
  await this.upgrade(this.store, this.store.version, DB_VERSION);
  this.store.version = DB_VERSION;  // 写入新版本
}

关键规则:

  • store.version 是数字,只能递增
  • 每次升级时比对 fromVersion(当前)和 toVersion(期望)
  • 如果 fromVersion 已 ≥ toVersion,跳过升级

二、ALTER TABLE ADD COLUMN

2.1 语法

HarmonyOS RDB 仅支持 ALTER TABLE ADD COLUMN,不支持修改列名、删除列或修改列类型:

ALTER TABLE table_name ADD COLUMN column_name data_type DEFAULT default_value;

2.2 实战:给 app_bindings 添加 expire_date 字段

假设 v1 → v2 需要新增一个过期日期字段:

const DB_VERSION = 2;

private async upgrade(store: relationalStore.RdbStore, fromVersion: number, toVersion: number): Promise<void> {
  // v1 初始化
  if (fromVersion < 1) {
    // ... 建表 SQL ...
  }

  // v2 升级:新增 expire_date
  if (fromVersion < 2) {
    const alterSql = `ALTER TABLE app_bindings ADD COLUMN expire_date INTEGER NOT NULL DEFAULT 0`;
    try {
      await store.executeSql(alterSql);
      hilog.info(0x0000, TAG, 'v2 升级完成:新增 expire_date');
    } catch (e) {
      hilog.error(0x0000, TAG, 'v2 升级失败: %{public}s', JSON.stringify(e));
      throw new Error('数据库升级失败(→v2):' + JSON.stringify(e));
    }

    // 同时添加新索引
    await store.executeSql(
      `CREATE INDEX IF NOT EXISTS idx_ab_expire ON app_bindings(expire_date)`
    );
  }
}

2.3 添加索引的最佳时机

索引不应写在建表阶段之后就无法变更。升级时也可以增量添加:

if (fromVersion < 2) {
  // 复合索引:在已有索引基础上补充
  await store.executeSql(
    `CREATE INDEX IF NOT EXISTS idx_ab_card_status ON app_bindings(card_id, status)`
  );
}

使用 IF NOT EXISTS 保证幂等性——无论执行多少次都不会报错。


三、实体模型同步更新

3.1 接口扩展

数据库升级后,TypeScript 接口也要同步新增字段:

export interface AppBindingEntity {
  id?: number;
  app_name: string;
  icon_key: string;
  category: string;
  card_id: number;
  status: string;
  remark: string;
  source: string;
  created_at: number;
  updated_at: number;
  // v2 新增
  expire_date?: number;  // 可选,兼容旧数据
}

3.2 DAO 层兼容

新增字段使用可选属性 (?),这样从旧版数据库查询出来的数据不会有类型错误。

写入时提供默认值:

async create(data: AppBindingEntity): Promise<number> {
  const now = Date.now();
  const entity = {
    ...data,
    expire_date: data.expire_date ?? 0  // 默认为 0(永不过期)
  };
  // ...
}

四、数据迁移策略

4.1 纯新增字段(推荐)

只做 ADD COLUMN + 默认值,旧数据自动填充默认值。

ALTER TABLE app_bindings ADD COLUMN expire_date INTEGER NOT NULL DEFAULT 0;

所有已有行的 expire_date 自动变为 0

4.2 字段重命名(有限支持)

RDB 不支持直接 RENAME COLUMN。替代方案:

  1. ALTER TABLE ADD COLUMN new_column
  2. UPDATE table SET new_column = old_column
  3. 在 Entity 层标记旧字段为 @deprecated
// v3 升级:phone → phone_number
if (fromVersion < 3) {
  await store.executeSql(
    `ALTER TABLE cards ADD COLUMN phone_number TEXT NOT NULL DEFAULT ''`
  );
  await store.executeSql(
    `UPDATE cards SET phone_number = phone`
  );
  // 旧字段保留,代码层不再使用
}

4.3 数据清洗

升级时还可以执行数据清洗:

if (fromVersion < 4) {
  // 将旧版的状态中文名标准化
  await store.executeSql(
    `UPDATE app_bindings SET status = '使用中' WHERE status = 'active'`
  );
  await store.executeSql(
    `UPDATE app_bindings SET status = '已停用' WHERE status = 'disabled'`
  );
}

五、完整的升级链路

把多个版本升级串联成一个完整的 switch/case 链:

const DB_VERSION = 5;

private async upgrade(store: relationalStore.RdbStore, fromVersion: number, toVersion: number): Promise<void> {
  // v1:建表
  if (fromVersion < 1) {
    await store.executeSql(`CREATE TABLE IF NOT EXISTS cards (...)`);
    await store.executeSql(`CREATE TABLE IF NOT EXISTS app_bindings (...)`);
    fromVersion = 1;
  }

  // v2:添加 expire_date
  if (fromVersion < 2) {
    await store.executeSql(`ALTER TABLE app_bindings ADD COLUMN expire_date INTEGER NOT NULL DEFAULT 0`);
    await store.executeSql(`CREATE INDEX IF NOT EXISTS idx_ab_expire ON app_bindings(expire_date)`);
    fromVersion = 2;
  }

  // v3:phone → phone_number 迁移
  if (fromVersion < 3) {
    await store.executeSql(`ALTER TABLE cards ADD COLUMN phone_number TEXT NOT NULL DEFAULT ''`);
    await store.executeSql(`UPDATE cards SET phone_number = phone`);
    fromVersion = 3;
  }

  // v4:新增 sync_status 表
  if (fromVersion < 4) {
    await store.executeSql(`CREATE TABLE IF NOT EXISTS sync_meta (...)`);
    fromVersion = 4;
  }

  // v5:清洗脏数据
  if (fromVersion < 5) {
    await store.executeSql(`UPDATE cards SET color = 'blue' WHERE color NOT IN ('blue','green','pink','orange','purple')`);
    fromVersion = 5;
  }

  hilog.info(0x0000, TAG, '数据库升级完成: v%{public}d → v%{public}d', fromVersion, toVersion);
}

六、降级防护

用户设备上可能安装了旧版本应用,打开新版数据库后又回退到旧版。此时 store.version 可能 > 应用的 DB_VERSION

private async upgrade(store: relationalStore.RdbStore, fromVersion: number, toVersion: number): Promise<void> {
  if (fromVersion > toVersion) {
    hilog.warn(0x0000, TAG, '检测到更高版本的数据库(v%{public}d),跳过降级', fromVersion);
    return;  // 不降级,保持兼容
  }
  // ...
}

核心原则:数据库只向前兼容。低版本 App 遇到高版本数据库,只读不写新增字段,不会崩溃。


七、升级前校验与回滚

7.1 升级前校验

private async preUpgradeCheck(store: relationalStore.RdbStore): Promise<void> {
  // 检查数据库是否可写
  await store.executeSql('SELECT 1');
  // 检查关键表是否存在
  const tables = ['cards', 'app_bindings'];
  for (const t of tables) {
    const result = await store.querySql(`SELECT name FROM sqlite_master WHERE type='table' AND name='${t}'`);
    if (!result.rowCount) {
      throw new Error(`关键表 ${t} 不存在`);
    }
    result.close();
  }
}

7.2 事务回滚

升级失败时,整个 executeSql 链会自动回滚吗?不会。HarmonyOS RDB 的每条 executeSql 默认是自动提交的。我们需要手动开启事务:

if (fromVersion < 2) {
  try {
    await store.beginTransaction();
    await store.executeSql('ALTER TABLE ...');
    await store.executeSql('CREATE INDEX ...');
    await store.commit();
  } catch (e) {
    await store.rollback();
    throw new Error('v2 升级失败,已回滚');
  }
}

八、单元测试

import { describe, it, expect } from '@ohos/hypium';

describe('DatabaseUpgradeTest', () => {
  it('版本号递增', 0, () => {
    expect(DB_VERSION).assertGreaterThan(0);
    expect(DB_VERSION).assertEqual(5);
  });

  it('ALTER TABLE 新增字段', 0, async () => {
    const store = DatabaseService.getInstance().getStore();
    await store.executeSql(`ALTER TABLE app_bindings ADD COLUMN test_col TEXT NOT NULL DEFAULT ''`);
    // 验证字段存在
    const result = await store.querySql(`PRAGMA table_info(app_bindings)`);
    // ...
  });
});

小结

要点 说明
版本管理 store.version 递增,由 upgrade() 手动控制
ALTER TABLE 仅支持 ADD COLUMN,不支持 DROP/RENAME COLUMN
默认值 NOT NULL + DEFAULT 保证旧数据兼容
索引 升级时用 IF NOT EXISTS 增量创建
降级防护 fromVersion > toVersion 时跳过
事务保护 复杂升级用 beginTransaction + commit 包裹
实体同步 TypeScript 接口新增可选属性

8.1 ALTER TABLE 语法参考

操作 语法 HarmonyOS RDB 支持
新增列 ALTER TABLE t ADD COLUMN c type DEFAULT v ✅ 支持
重命名列 ALTER TABLE t RENAME COLUMN old TO new ❌ 不支持
删除列 ALTER TABLE t DROP COLUMN c ❌ 不支持
重命名表 ALTER TABLE t RENAME TO new_t ✅ 支持

8.2 版本升级历史

版本 变更内容 SQL 操作
v1 建表(card / app_binding / sms_candidate) CREATE TABLE
v2 新增 expire_date 字段 + 索引 ADD COLUMN + CREATE INDEX
v3 phone → phone_number 迁移 ADD COLUMN + UPDATE
v4 新增 sync_meta 表 CREATE TABLE
v5 清洗脏数据 UPDATE … SET

至此,数据库全系列(26-40)全部完成!从 RDB 概念、DatabaseService 初始化、三个 Entity/DAO 设计、批量操作、错误处理、查询优化、数据统计、导出备份、导入恢复到版本升级,构成了完整的持久化链路。

从第 41 篇开始,我们进入核心页面开发阶段——用 ArkUI 组件把数据模型转化为可交互的界面。


如果这篇文章对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,你的支持是我持续创作的动力!


相关资源:

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐