HarmonyOS 新手入门:ArkData 关系型数据库,用事务解决“写一半”的尴尬
【摘要】 数据库里最怕什么?不是写失败,而是写了一半。 比如你在本地保存一批账单,第一条餐饮写进去了,第二条通勤失败了。页面再一刷新,用户看到一半数据,后面就很难解释。这种场景就该用事务。 官方文档可以先放在手边: 通过关系型数据库实现数据持久化 relationalStore API 参考 先说结论 事务不是“高级写法”,它只
HarmonyOS 新手入门:ArkData 关系型数据库,用事务解决“写一半”的尴尬
数据库里最怕什么?不是写失败,而是写了一半。
比如你在本地保存一批账单,第一条餐饮写进去了,第二条通勤失败了。页面再一刷新,用户看到一半数据,后面就很难解释。这种场景就该用事务。
官方文档可以先放在手边:
先说结论
事务不是“高级写法”,它只是用来保证数据别变成半成品。
一句话:要么都写进去,要么都别留下。
它怎么用
先创建事务:
const transaction = await store.createTransaction();
多条写入都成功,就提交:
await transaction.insert(TABLE_BILL, food);
await transaction.insert(TABLE_BILL, traffic);
await transaction.commit();
中间任何一步失败,就回滚:
try {
// 多条写入
} catch (err) {
await transaction.rollback();
}
什么场景值得用
不是所有写入都要包事务。单条保存没必要搞复杂。
比较适合事务的场景有这些:
- 批量导入账单
- 创建订单和订单明细
- 保存草稿和附件索引
- 离线任务队列批量入库
- 需要保持本地数据一致性的多表写入
这个 Demo 做什么
Demo 里有一个“模拟失败”开关。
关闭时,事务会写入两条账单并 commit。开启时,第一条写入后主动抛错,然后执行 rollback,数据库里不会留下这次批次的半截数据。
点击“查看”会打开半模态弹框,展示事务结果、数据库名、表名和当前记录数。列表只展示当前账单数据,不做 LazyForEach 懒加载渲染处理。
欢迎评论区一起分享~
你们项目里哪些本地写入必须用事务?订单、账单、购物车,还是离线任务队列?如果遇到过“写了一半失败”的真实问题,也可以把场景发出来,后面可以继续拆。
完整示例代码
文件位置:ArkDataRdbTransactionDemo.ets
import { common } from '@kit.AbilityKit';
import { relationalStore } from '@kit.ArkData';
import { promptAction, router } from '@kit.ArkUI';
const DB_NAME: string = 'rdb_transaction_demo.db';
const TABLE_BILL: string = 'bill_items';
class BillItem {
id: number = 0;
name: string = '';
amount: number = 0;
batchNo: string = '';
constructor(id: number, name: string, amount: number, batchNo: string) {
this.id = id;
this.name = name;
this.amount = amount;
this.batchNo = batchNo;
}
}
class TransactionInfoItem {
label: string = '';
value: string = '';
constructor(label: string, value: string) {
this.label = label;
this.value = value;
}
}
@Entry
@Component
struct ArkDataRdbTransactionDemo {
@State batchName: string = '六月账单';
@State shouldFail: boolean = false;
@State tips: string = '事务会把多条写入当成一个整体';
@State items: BillItem[] = [];
@State showResultSheet: boolean = false;
@State infoItems: TransactionInfoItem[] = [];
private store: relationalStore.RdbStore | undefined = undefined;
aboutToAppear(): void {
this.loadBills();
}
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_BILL} (` +
'id INTEGER PRIMARY KEY AUTOINCREMENT, ' +
'name TEXT NOT NULL, ' +
'amount REAL NOT NULL, ' +
'batch_no TEXT NOT NULL)'
);
this.store = store;
return store;
}
private updateInfoItems(result: string): void {
this.infoItems = [
new TransactionInfoItem('数据库名称', DB_NAME),
new TransactionInfoItem('表名', TABLE_BILL),
new TransactionInfoItem('事务结果', result),
new TransactionInfoItem('当前记录数', `${this.items.length}`),
new TransactionInfoItem('失败开关', this.shouldFail ? '开启:会触发 rollback' : '关闭:会 commit'),
new TransactionInfoItem('说明', '列表只展示当前账单数据,不做 LazyForEach 懒加载渲染处理')
];
}
private async loadBills(result: string = '等待事务执行'): Promise<void> {
try {
const store = await this.getStore();
const resultSet = await store.querySql(
`SELECT id, name, amount, batch_no FROM ${TABLE_BILL} ORDER BY id DESC`
);
const rows: BillItem[] = [];
const idIndex = resultSet.getColumnIndex('id');
const nameIndex = resultSet.getColumnIndex('name');
const amountIndex = resultSet.getColumnIndex('amount');
const batchIndex = resultSet.getColumnIndex('batch_no');
while (resultSet.goToNextRow()) {
rows.push(new BillItem(
resultSet.getLong(idIndex),
resultSet.getString(nameIndex),
resultSet.getDouble(amountIndex),
resultSet.getString(batchIndex)
));
}
resultSet.close();
this.items = rows;
this.updateInfoItems(result);
} catch (err) {
this.tips = '读取失败,请查看日志';
promptAction.showToast({ message: '读取失败' });
console.error('RDB transaction load failed');
}
}
private async runTransaction(): Promise<void> {
const store = await this.getStore();
const transaction = await store.createTransaction();
try {
const batchNo = `${this.batchName}-${new Date().getTime()}`;
const food: relationalStore.ValuesBucket = {
name: '餐饮',
amount: 38.5,
batch_no: batchNo
};
const traffic: relationalStore.ValuesBucket = {
name: '通勤',
amount: 12,
batch_no: batchNo
};
await transaction.insert(TABLE_BILL, food);
if (this.shouldFail) {
throw new Error('mock transaction failure');
}
await transaction.insert(TABLE_BILL, traffic);
await transaction.commit();
this.tips = '事务提交成功:两条账单都已写入';
await this.loadBills('commit 成功');
promptAction.showToast({ message: '事务已提交' });
} catch (err) {
await transaction.rollback();
this.tips = '事务已回滚:中间失败时不会留下半条数据';
await this.loadBills('rollback 已执行');
promptAction.showToast({ message: '事务已回滚' });
console.error('RDB transaction rollback');
}
}
private async clearBills(): Promise<void> {
try {
const store = await this.getStore();
await store.executeSql(`DELETE FROM ${TABLE_BILL}`);
await this.loadBills('已清空');
this.tips = '账单表已清空';
promptAction.showToast({ message: '已清空' });
} catch (err) {
this.tips = '清空失败,请查看日志';
promptAction.showToast({ message: '清空失败' });
console.error('RDB transaction clear failed');
}
}
private async showTransactionInfo(): Promise<void> {
await this.loadBills();
this.showResultSheet = true;
}
@Builder
private transactionInfoSheet() {
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: TransactionInfoItem) => {
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: TransactionInfoItem) => item.label)
}
.width('100%')
Button('关闭')
.width('100%')
.height(44)
.fontSize(16)
.backgroundColor('#0F766E')
.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('#0F766E')
.onClick(() => {
router.back();
})
Blank()
}
.width('100%')
Text('RDB 事务')
.width('100%')
.fontSize(28)
.fontWeight(700)
.fontColor('#182431')
Text('多条写入要么一起成功,要么一起回滚。')
.width('100%')
.fontSize(14)
.fontColor('#56616F')
TextInput({ placeholder: '输入批次名称', text: this.batchName })
.height(44)
.fontSize(16)
.backgroundColor('#F0FDFA')
.fontColor('#0F172A')
.onChange((value: string) => {
this.batchName = value;
})
Button(this.shouldFail ? '模拟失败:开启' : '模拟失败:关闭')
.width('100%')
.height(42)
.fontSize(15)
.fontColor(this.shouldFail ? '#FFFFFF' : '#0F172A')
.backgroundColor(this.shouldFail ? '#DC2626' : '#CCFBF1')
.onClick(() => {
this.shouldFail = !this.shouldFail;
this.updateInfoItems('等待事务执行');
})
Row({ space: 10 }) {
Button('执行事务')
.layoutWeight(1)
.height(44)
.fontSize(15)
.backgroundColor('#0F766E')
.onClick(() => {
this.runTransaction();
})
Button('查看')
.layoutWeight(1)
.height(44)
.fontSize(15)
.fontColor('#0F172A')
.backgroundColor('#99F6E4')
.onClick(() => {
this.showTransactionInfo();
})
Button('清空')
.layoutWeight(1)
.height(44)
.fontSize(15)
.fontColor('#0F172A')
.backgroundColor('#CBD5E1')
.onClick(() => {
this.clearBills();
})
}
.width('100%')
Column({ space: 8 }) {
ForEach(this.items, (item: BillItem) => {
Column({ space: 4 }) {
Text(`${item.name}:${item.amount}`)
.width('100%')
.fontSize(17)
.fontWeight(700)
.fontColor('#0F172A')
Text(`id=${item.id} · batch=${item.batchNo}`)
.width('100%')
.fontSize(12)
.fontColor('#64748B')
}
.width('100%')
.padding(14)
.backgroundColor('#F8FAFC')
.borderRadius(8)
}, (item: BillItem) => `${item.id}`)
}
.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.transactionInfoSheet(), {
height: SheetSize.MEDIUM,
dragBar: true,
showClose: true,
onDisappear: () => {
this.showResultSheet = false;
}
})
}
}
【版权声明】本文为华为云社区用户原创内容,未经允许不得转载,如需转载请自行联系原作者进行授权。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱:
cloudbbs@huaweicloud.com
- 点赞
- 收藏
- 关注作者
评论(0)