HarmonyOS 新手入门:ArkData 关系型数据库,Sendable 不是新数据库
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;
}
})
}
}
- 点赞
- 收藏
- 关注作者
评论(0)