HarmonyOS 新手入门:ArkData 关系型数据库,Sendable 不是新数据库

举报
蓝瘦的蜕变 发表于 2026/07/02 10:47:51 2026/07/02
【摘要】 关系型数据库这一组,前面已经写了建表、CRUD、升级和事务。还剩一个容易被新手误会的点:sendableRelationalStore。 它不是另一个 RDB 引擎,也不是“分布式关系型数据库”。它更像一组工具方法,用来处理可以跨线程传递的数据类型。关系型数据库真正写入时,还是回到 relationalStore.Rd

HarmonyOS 新手入门:ArkData 关系型数据库,Sendable 不是新数据库

关系型数据库这一组,前面已经写了建表、CRUD、升级和事务。还剩一个容易被新手误会的点:sendableRelationalStore

它不是另一个 RDB 引擎,也不是“分布式关系型数据库”。它更像一组工具方法,用来处理可以跨线程传递的数据类型。关系型数据库真正写入时,还是回到 relationalStore.RdbStore

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

先说结论

sendableRelationalStore 不是新数据库。

它解决的是多线程或后台任务里“数据能不能安全传递”的问题。后台任务可以准备 Sendable 数据,真正写库时还是由 RDB Store 完成。

它怎么用

做多线程或后台任务时,数据不是想传什么就传什么。跨线程传递的数据要满足 Sendable 约束,否则容易在任务传递、数据转换时出问题。

sendableRelationalStore 里比较常用的是这些转换方法:

const sendableBucket = sendableRelationalStore.toSendableValuesBucket(normalBucket);
const normalBucket = sendableRelationalStore.fromSendableValuesBucket(sendableBucket);

普通 ValuesBucket 可以转成 Sendable ValuesBucket。反过来也可以从 Sendable 转回普通对象。

写入还是用 RdbStore

当前 SDK 里,RdbStore 提供了支持 Sendable ValuesBucket 的同步插入接口:

const rowId = store.insertSync(TABLE_LOG, sendableBucket);

所以这篇 Demo 的重点不是“开一个线程”,而是先把 RDB 和 Sendable 数据类型的关系讲清楚:后台任务可以准备 Sendable 数据,真正写库时由 RDB Store 完成。

这个 Demo 做什么

页面里可以输入批次标题,选择生成 3、5 或 8 条数据。

点击“转换并写入”后,Demo 会:

  • 生成普通 relationalStore.ValuesBucket
  • 转成 sendableRelationalStore.ValuesBucket
  • 调用 insertSync 写入 RDB
  • 读取数据库列表并展示
  • 点击“查看”时,用半模态弹框展示转换信息

列表只展示当前保存的数据库信息,不做 LazyForEach 懒加载渲染处理。这里讲的是 Sendable 数据类型,不是大列表优化。

什么时候继续往多线程讲

如果你的场景只是普通表单保存,不需要 Sendable。

更适合继续展开多线程的是这些场景:

  • 批量导入本地文件数据
  • 后台解析 CSV / JSON 后写入 RDB
  • 大量数据清洗后再入库
  • Worker 或 taskpool 里准备数据,UI 线程负责展示结果

后面如果继续写多线程 IO,可以把这篇当成铺垫:先让读者知道 RDB 的 Sendable 数据长什么样,再讲任务线程怎么组织。

欢迎评论区一起分享~

你们项目里有没有遇到过“后台整理数据,再写入本地数据库”的场景?比如导入账单、解析文件、同步缓存。如果有,你会选择 taskpool、Worker,还是直接在页面里分批处理?

完整示例代码

文件位置:ArkDataRdbSendableDemo.ets

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

const DB_NAME: string = 'rdb_sendable_demo.db';
const TABLE_LOG: string = 'sendable_logs';

class SendableLogItem {
  id: number = 0;
  title: string = '';
  source: string = '';
  cost: number = 0;
  createdAt: string = '';

  constructor(id: number, title: string, source: string, cost: number, createdAt: string) {
    this.id = id;
    this.title = title;
    this.source = source;
    this.cost = cost;
    this.createdAt = createdAt;
  }
}

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

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

@Entry
@Component
struct ArkDataRdbSendableDemo {
  @State batchTitle: string = '后台整理数据';
  @State batchSize: number = 3;
  @State tips: string = '把普通 ValuesBucket 转成 Sendable ValuesBucket 后,再用 insertSync 写入 RDB';
  @State items: SendableLogItem[] = [];
  @State infoItems: SendableInfoItem[] = [];
  @State showResultSheet: boolean = false;

  private store: relationalStore.RdbStore | undefined = undefined;
  private lastNormalText: string = '还没有准备数据';
  private lastSendableText: string = '还没有转换数据';

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

  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_LOG} (` +
        'id INTEGER PRIMARY KEY AUTOINCREMENT, ' +
        'title TEXT NOT NULL, ' +
        'source TEXT NOT NULL, ' +
        'cost INTEGER NOT NULL, ' +
        'created_at TEXT NOT NULL)'
    );
    this.store = store;
    return store;
  }

  private createNormalBucket(title: string, cost: number): relationalStore.ValuesBucket {
    return {
      title: title,
      source: 'sendable',
      cost: cost,
      created_at: new Date().toString()
    };
  }

  private updateInfoItems(action: string): void {
    this.infoItems = [
      new SendableInfoItem('数据库名称', DB_NAME),
      new SendableInfoItem('表名', TABLE_LOG),
      new SendableInfoItem('最近动作', action),
      new SendableInfoItem('批量条数', `${this.batchSize}`),
      new SendableInfoItem('当前记录数', `${this.items.length}`),
      new SendableInfoItem('普通 ValuesBucket', this.lastNormalText),
      new SendableInfoItem('Sendable ValuesBucket', this.lastSendableText),
      new SendableInfoItem('说明', '列表只展示当前保存的数据库信息,不做 LazyForEach 懒加载渲染处理')
    ];
  }

  private async loadLogs(action: string = '等待写入'): Promise<void> {
    try {
      const store = await this.getStore();
      const resultSet = await store.querySql(
        `SELECT id, title, source, cost, created_at FROM ${TABLE_LOG} ORDER BY id DESC`
      );
      const rows: SendableLogItem[] = [];
      const idIndex = resultSet.getColumnIndex('id');
      const titleIndex = resultSet.getColumnIndex('title');
      const sourceIndex = resultSet.getColumnIndex('source');
      const costIndex = resultSet.getColumnIndex('cost');
      const createdAtIndex = resultSet.getColumnIndex('created_at');
      while (resultSet.goToNextRow()) {
        rows.push(new SendableLogItem(
          resultSet.getLong(idIndex),
          resultSet.getString(titleIndex),
          resultSet.getString(sourceIndex),
          resultSet.getLong(costIndex),
          resultSet.getString(createdAtIndex)
        ));
      }
      resultSet.close();
      this.items = rows;
      this.updateInfoItems(action);
    } catch (err) {
      this.tips = '读取失败,请查看日志';
      promptAction.showToast({ message: '读取失败' });
      console.error('RDB sendable load failed');
    }
  }

  private async insertSendableBatch(): Promise<void> {
    try {
      const store = await this.getStore();
      let successCount = 0;
      for (let index = 1; index <= this.batchSize; index++) {
        const prefix = this.batchTitle.length > 0 ? this.batchTitle : '后台整理数据';
        const title = `${prefix}-${index}`;
        const cost = 20 + index;
        const normalBucket = this.createNormalBucket(title, cost);
        const sendableBucket = sendableRelationalStore.toSendableValuesBucket(normalBucket);
        const normalAgain = sendableRelationalStore.fromSendableValuesBucket(sendableBucket);
        if (index === 1) {
          this.lastNormalText = `title=${title}, cost=${cost}`;
          this.lastSendableText = `已转换并转回普通 ValuesBucket:${JSON.stringify(normalAgain)}`;
        }
        const rowId = store.insertSync(TABLE_LOG, sendableBucket);
        if (rowId > 0) {
          successCount++;
        }
      }
      await this.loadLogs(`insertSync 写入 ${successCount} 条`);
      this.tips = `写入成功:${successCount} 条 Sendable 数据已保存`;
      promptAction.showToast({ message: 'Sendable 数据已保存' });
    } catch (err) {
      this.tips = '写入失败,请查看日志';
      promptAction.showToast({ message: '写入失败' });
      console.error('RDB sendable insert failed');
    }
  }

  private async clearLogs(): Promise<void> {
    try {
      const store = await this.getStore();
      await store.executeSql(`DELETE FROM ${TABLE_LOG}`);
      this.lastNormalText = '已清空';
      this.lastSendableText = '已清空';
      await this.loadLogs('清空表数据');
      this.tips = '数据已清空';
      promptAction.showToast({ message: '已清空' });
    } catch (err) {
      this.tips = '清空失败,请查看日志';
      promptAction.showToast({ message: '清空失败' });
      console.error('RDB sendable clear failed');
    }
  }

  private async showSendableInfo(): Promise<void> {
    await this.loadLogs('查看 Sendable 信息');
    this.showResultSheet = true;
  }

  @Builder
  private sendableInfoSheet() {
    Column({ space: 14 }) {
      Text('RDB Sendable 写入信息')
        .width('100%')
        .fontSize(22)
        .fontWeight(700)
        .fontColor('#0F172A')

      Text('这里展示普通 ValuesBucket 和 Sendable ValuesBucket 的转换结果。列表只展示当前数据库信息,不做 LazyForEach 懒加载渲染处理。')
        .width('100%')
        .fontSize(13)
        .fontColor('#64748B')

      Column({ space: 8 }) {
        ForEach(this.infoItems, (item: SendableInfoItem) => {
          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: SendableInfoItem) => item.label)
      }
      .width('100%')

      Button('关闭')
        .width('100%')
        .height(44)
        .fontSize(16)
        .backgroundColor('#155E75')
        .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('#155E75')
            .onClick(() => {
              router.back();
            })
          Blank()
        }
        .width('100%')

        Text('RDB Sendable 写入')
          .width('100%')
          .fontSize(28)
          .fontWeight(700)
          .fontColor('#182431')

        Text('演示普通 ValuesBucket 与 Sendable ValuesBucket 的转换,并用 insertSync 写入 RDB。')
          .width('100%')
          .fontSize(14)
          .fontColor('#56616F')

        TextInput({ placeholder: '输入批次标题', text: this.batchTitle })
          .height(44)
          .fontSize(16)
          .backgroundColor('#ECFEFF')
          .fontColor('#0F172A')
          .onChange((value: string) => {
            this.batchTitle = value;
          })

        Row({ space: 10 }) {
          Button('3 条')
            .layoutWeight(1)
            .height(40)
            .fontSize(14)
            .fontColor(this.batchSize === 3 ? '#FFFFFF' : '#0F172A')
            .backgroundColor(this.batchSize === 3 ? '#155E75' : '#CFFAFE')
            .onClick(() => {
              this.batchSize = 3;
            })
          Button('5 条')
            .layoutWeight(1)
            .height(40)
            .fontSize(14)
            .fontColor(this.batchSize === 5 ? '#FFFFFF' : '#0F172A')
            .backgroundColor(this.batchSize === 5 ? '#155E75' : '#CFFAFE')
            .onClick(() => {
              this.batchSize = 5;
            })
          Button('8 条')
            .layoutWeight(1)
            .height(40)
            .fontSize(14)
            .fontColor(this.batchSize === 8 ? '#FFFFFF' : '#0F172A')
            .backgroundColor(this.batchSize === 8 ? '#155E75' : '#CFFAFE')
            .onClick(() => {
              this.batchSize = 8;
            })
        }
        .width('100%')

        Row({ space: 10 }) {
          Button('转换并写入')
            .layoutWeight(1)
            .height(44)
            .fontSize(15)
            .backgroundColor('#155E75')
            .onClick(() => {
              this.insertSendableBatch();
            })
          Button('查看')
            .layoutWeight(1)
            .height(44)
            .fontSize(15)
            .fontColor('#0F172A')
            .backgroundColor('#A5F3FC')
            .onClick(() => {
              this.showSendableInfo();
            })
          Button('清空')
            .layoutWeight(1)
            .height(44)
            .fontSize(15)
            .fontColor('#0F172A')
            .backgroundColor('#CBD5E1')
            .onClick(() => {
              this.clearLogs();
            })
        }
        .width('100%')

        Column({ space: 8 }) {
          ForEach(this.items, (item: SendableLogItem) => {
            Column({ space: 4 }) {
              Text(item.title)
                .width('100%')
                .fontSize(17)
                .fontWeight(700)
                .fontColor('#0F172A')
              Text(`id=${item.id} · source=${item.source} · cost=${item.cost}`)
                .width('100%')
                .fontSize(12)
                .fontColor('#64748B')
              Text(item.createdAt)
                .width('100%')
                .fontSize(12)
                .fontColor('#94A3B8')
            }
            .width('100%')
            .padding(14)
            .backgroundColor('#F8FAFC')
            .borderRadius(8)
          }, (item: SendableLogItem) => `${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.sendableInfoSheet(), {
      height: SheetSize.MEDIUM,
      dragBar: true,
      showClose: true,
      onDisappear: () => {
        this.showResultSheet = false;
      }
    })
  }
}
【版权声明】本文为华为云社区用户原创内容,未经允许不得转载,如需转载请自行联系原作者进行授权。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

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

全部回复

上滑加载中

设置昵称

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

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

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