HarmonyOS 新手入门:ArkData 关系型数据库,表结构升级要提前留口子

举报
蓝瘦的蜕变 发表于 2026/06/30 10:01:41 2026/06/30
【摘要】 本地数据库最容易被忽略的一件事,就是升级。 第一版上线时你可能只存一个 title。过几天产品说笔记要加分类,于是你想加一个 category 字段。新用户当然没问题,老用户手机里已经有旧表了,这时就不能靠删库重建糊过去。 这篇就用一个很小的例子,演示 RDB 怎么处理表结构升级。 官方文档可以先放在手边: 通过关系型

HarmonyOS 新手入门:ArkData 关系型数据库,表结构升级要提前留口子

本地数据库最容易被忽略的一件事,就是升级。

第一版上线时你可能只存一个 title。过几天产品说笔记要加分类,于是你想加一个 category 字段。新用户当然没问题,老用户手机里已经有旧表了,这时就不能靠删库重建糊过去。

这篇就用一个很小的例子,演示 RDB 怎么处理表结构升级。

官方文档可以先放在手边:

先说结论

本地表结构不是只给新用户看的。老用户手机里已经有旧表时,升级要靠版本和迁移逻辑,而不是删库重建。

一句话:先判断版本,再执行迁移,最后更新版本。

它怎么用

打开数据库后,可以通过 store.version 读取和设置版本号。

Demo 里目标版本是 2。如果当前版本还没到 2,就补一个 category 字段:

if (store.version < 1) {
  store.version = 1;
}
if (store.version < TARGET_VERSION) {
  await this.upgradeToVersion2(store);
}

真正迁移字段时,用一条普通的 SQL 就够了:

await store.executeSql(
  `ALTER TABLE ${TABLE_NOTE} ADD COLUMN category TEXT DEFAULT '未分类'`
);
store.version = TARGET_VERSION;

这段代码看起来简单,但思路很重要:先判断版本,再执行迁移,最后更新版本。不要一打开数据库就无脑执行 ALTER TABLE

容易踩坑的地方

如果版本从 1 升到 4,中间最好按顺序处理:

  • 1 -> 2:新增分类字段
  • 2 -> 3:新增更新时间字段
  • 3 -> 4:调整索引

不要只写一个“大升级函数”。用户可能从任何旧版本升级上来,跳过中间步骤很容易漏数据。

这个 Demo 做什么

Demo 页面会展示当前版本、目标版本和记录数。点击“查看”时,半模态弹框会列出数据库名、表名、当前版本、新增字段等信息。

列表只展示当前保存的数据库信息,不做 LazyForEach 懒加载渲染处理。这里关注的是迁移流程,不是列表渲染。

欢迎评论区一起分享~

你们做本地数据库升级时,一般怎么管版本?SQL 直接写代码里,还是单独维护迁移脚本?如果踩过字段重复、升级中断、老数据兼容这些坑,也可以在评论区补充。

完整示例代码

文件位置:ArkDataRdbUpgradeDemo.ets

import { common } from '@kit.AbilityKit';
import { relationalStore } from '@kit.ArkData';
import { promptAction, router } from '@kit.ArkUI';

const DB_NAME: string = 'rdb_upgrade_demo.db';
const TABLE_NOTE: string = 'upgrade_notes';
const TARGET_VERSION: number = 2;

class UpgradeInfoItem {
  label: string = '';
  value: string = '';

  constructor(label: string, value: string) {
    this.label = label;
    this.value = value;
  }
}

@Entry
@Component
struct ArkDataRdbUpgradeDemo {
  @State noteTitle: string = 'RDB 版本迁移';
  @State category: string = '学习笔记';
  @State currentVersion: number = 0;
  @State recordCount: number = 0;
  @State tips: string = '打开数据库后检查版本,低版本会自动补字段';
  @State showResultSheet: boolean = false;
  @State infoItems: UpgradeInfoItem[] = [];

  private store: relationalStore.RdbStore | undefined = undefined;

  aboutToAppear(): void {
    this.prepareDatabase();
  }

  private async getStore(): Promise<relationalStore.RdbStore> {
    if (this.store !== undefined) {
      return this.store;
    }

    const context = getContext(this) as common.UIAbilityContext;
    const config: relationalStore.StoreConfig = {
      name: DB_NAME,
      securityLevel: relationalStore.SecurityLevel.S1
    };
    const store = await relationalStore.getRdbStore(context, config);
    await store.executeSql(
      `CREATE TABLE IF NOT EXISTS ${TABLE_NOTE} (` +
        'id INTEGER PRIMARY KEY AUTOINCREMENT, ' +
        'title TEXT NOT NULL)'
    );
    this.store = store;
    return store;
  }

  private async prepareDatabase(): Promise<void> {
    try {
      const store = await this.getStore();
      if (store.version < 1) {
        store.version = 1;
      }
      if (store.version < TARGET_VERSION) {
        await this.upgradeToVersion2(store);
      }
      this.currentVersion = store.version;
      await this.refreshInfo();
      this.tips = '数据库已检查:当前版本可用';
    } catch (err) {
      this.tips = '数据库检查失败,请查看日志';
      promptAction.showToast({ message: '检查失败' });
      console.error('RDB upgrade prepare failed');
    }
  }

  private async upgradeToVersion2(store: relationalStore.RdbStore): Promise<void> {
    try {
      await store.executeSql(`ALTER TABLE ${TABLE_NOTE} ADD COLUMN category TEXT DEFAULT '未分类'`);
    } catch (err) {
      console.info('category column may already exist');
    }
    store.version = TARGET_VERSION;
  }

  private async refreshInfo(): Promise<void> {
    const store = await this.getStore();
    const resultSet = await store.querySql(`SELECT COUNT(*) AS total FROM ${TABLE_NOTE}`);
    let count = 0;
    if (resultSet.goToNextRow()) {
      count = resultSet.getLong(resultSet.getColumnIndex('total'));
    }
    resultSet.close();
    this.currentVersion = store.version;
    this.recordCount = count;
    this.infoItems = [
      new UpgradeInfoItem('数据库名称', DB_NAME),
      new UpgradeInfoItem('表名', TABLE_NOTE),
      new UpgradeInfoItem('目标版本', `${TARGET_VERSION}`),
      new UpgradeInfoItem('当前版本', `${this.currentVersion}`),
      new UpgradeInfoItem('新增字段', 'category TEXT DEFAULT 未分类'),
      new UpgradeInfoItem('当前记录数', `${this.recordCount}`),
      new UpgradeInfoItem('说明', '列表只展示版本和字段信息,不做 LazyForEach 懒加载渲染处理')
    ];
  }

  private async insertNote(): Promise<void> {
    try {
      const store = await this.getStore();
      await this.prepareDatabase();
      const values: relationalStore.ValuesBucket = {
        title: this.noteTitle.length > 0 ? this.noteTitle : '未命名笔记',
        category: this.category.length > 0 ? this.category : '未分类'
      };
      await store.insert(TABLE_NOTE, values);
      await this.refreshInfo();
      this.tips = '保存成功:新字段 category 已参与写入';
      promptAction.showToast({ message: '笔记已保存' });
    } catch (err) {
      this.tips = '保存失败,请查看日志';
      promptAction.showToast({ message: '保存失败' });
      console.error('RDB upgrade insert failed');
    }
  }

  private async showUpgradeInfo(): Promise<void> {
    await this.refreshInfo();
    this.showResultSheet = true;
  }

  @Builder
  private upgradeInfoSheet() {
    Column({ space: 14 }) {
      Text('RDB 版本升级信息')
        .width('100%')
        .fontSize(22)
        .fontWeight(700)
        .fontColor('#0F172A')

      Text('这里展示当前数据库版本、目标版本和新增字段信息,不做 LazyForEach 懒加载渲染处理。')
        .width('100%')
        .fontSize(13)
        .fontColor('#64748B')

      Column({ space: 8 }) {
        ForEach(this.infoItems, (item: UpgradeInfoItem) => {
          Column({ space: 4 }) {
            Text(item.label)
              .fontSize(12)
              .fontColor('#64748B')
            Text(item.value)
              .fontSize(16)
              .fontColor('#0F172A')
          }
          .width('100%')
          .padding(12)
          .backgroundColor('#F8FAFC')
          .borderRadius(8)
        }, (item: UpgradeInfoItem) => item.label)
      }
      .width('100%')

      Button('关闭')
        .width('100%')
        .height(44)
        .fontSize(16)
        .backgroundColor('#7C3AED')
        .onClick(() => {
          this.showResultSheet = false;
        })
    }
    .width('100%')
    .padding({ left: 20, right: 20, top: 12, bottom: 20 })
  }

  build() {
    Scroll() {
      Column({ space: 18 }) {
        Row() {
          Text('返回')
            .fontSize(14)
            .fontColor('#7C3AED')
            .onClick(() => {
              router.back();
            })
          Blank()
        }
        .width('100%')

        Text('RDB 表结构升级')
          .width('100%')
          .fontSize(28)
          .fontWeight(700)
          .fontColor('#182431')

        Text('用 store.version 判断版本,低版本自动 ALTER TABLE。')
          .width('100%')
          .fontSize(14)
          .fontColor('#56616F')

        TextInput({ placeholder: '输入笔记标题', text: this.noteTitle })
          .height(44)
          .fontSize(16)
          .backgroundColor('#F5F3FF')
          .fontColor('#0F172A')
          .onChange((value: string) => {
            this.noteTitle = value;
          })

        TextInput({ placeholder: '输入分类', text: this.category })
          .height(44)
          .fontSize(16)
          .backgroundColor('#F5F3FF')
          .fontColor('#0F172A')
          .onChange((value: string) => {
            this.category = value;
          })

        Column({ space: 8 }) {
          Text(`当前版本:${this.currentVersion}`)
            .width('100%')
            .fontSize(18)
            .fontWeight(700)
            .fontColor('#0F172A')
          Text(`记录数:${this.recordCount} · 目标版本:${TARGET_VERSION}`)
            .width('100%')
            .fontSize(14)
            .fontColor('#64748B')
        }
        .width('100%')
        .padding(16)
        .backgroundColor('#F8FAFC')
        .borderRadius(8)

        Row({ space: 10 }) {
          Button('检查升级')
            .layoutWeight(1)
            .height(44)
            .fontSize(15)
            .backgroundColor('#7C3AED')
            .onClick(() => {
              this.prepareDatabase();
            })
          Button('保存笔记')
            .layoutWeight(1)
            .height(44)
            .fontSize(15)
            .fontColor('#0F172A')
            .backgroundColor('#DDD6FE')
            .onClick(() => {
              this.insertNote();
            })
          Button('查看')
            .layoutWeight(1)
            .height(44)
            .fontSize(15)
            .fontColor('#0F172A')
            .backgroundColor('#CBD5E1')
            .onClick(() => {
              this.showUpgradeInfo();
            })
        }
        .width('100%')

        Text(this.tips)
          .width('100%')
          .fontSize(13)
          .fontColor('#64748B')
      }
      .width('100%')
      .padding(24)
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#FFFFFF')
    .bindSheet(this.showResultSheet, this.upgradeInfoSheet(), {
      height: SheetSize.MEDIUM,
      dragBar: true,
      showClose: true,
      onDisappear: () => {
        this.showResultSheet = false;
      }
    })
  }
}
【版权声明】本文为华为云社区用户原创内容,未经允许不得转载,如需转载请自行联系原作者进行授权。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。