HarmonyOS 新手入门:ArkData 关系型数据库,用事务解决“写一半”的尴尬

举报
蓝瘦的蜕变 发表于 2026/07/01 10:01:04 2026/07/01
【摘要】 数据库里最怕什么?不是写失败,而是写了一半。 比如你在本地保存一批账单,第一条餐饮写进去了,第二条通勤失败了。页面再一刷新,用户看到一半数据,后面就很难解释。这种场景就该用事务。 官方文档可以先放在手边: 通过关系型数据库实现数据持久化 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

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

全部回复

上滑加载中

设置昵称

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

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

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