HarmonyOS 新手入门:ArkData 关系型数据库,表结构升级要提前留口子
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;
}
})
}
}
- 点赞
- 收藏
- 关注作者
评论(0)