HarmonyOS 新手入门:ArkData 关系型数据库跨设备同步,分布式表不是普通表
HarmonyOS 新手入门:ArkData 关系型数据库跨设备同步,分布式表不是普通表
RDB 前面已经写过建表、CRUD、升级、事务和 Sendable。再往多设备场景走,就会遇到一个容易混的点:关系型数据库也能做跨设备同步,但它不是“把普通表自动复制到另一台设备”。
官方文档《关系型数据库跨设备数据同步 (ArkTS)》里讲得很明确:先把表设置为分布式表,再通过同步接口在可信设备之间同步数据。API 23 之前默认是多设备协同表模式;API 23 起还支持单版本表模式。
官方文档可以先放在手边:
先说结论
如果你的数据是结构化的,比如任务、订单、记录列表,并且后面还要按字段查询,RDB 跨设备同步比 KVStore 更适合。
但它不适合一上来就拿来做“实时协同编辑”。新手先记住这条链路:
建表 -> setDistributedTables -> insert/update -> sync -> on('dataChange') -> 查询远端或分布式表
这里的关键不是 SQL 多复杂,而是表模式和同步边界要搞清楚。
多设备协同表和单版本表
官方文档里有两个模式:
- 多设备协同表模式:远端设备同步过来的数据会进入按 deviceId 隔离的分布式表。需要通过
obtainDistributedTableName拿到对应表名再查。 - 单版本表模式:API 23 起支持,数据直接进入本地表,但要配置
sync_schema.json指定同步列和冲突列。
这篇 Demo 先用多设备协同表模式,因为它更适合入门,也不需要先讲 schema 文件。
这个 Demo 做什么
页面会做几件事:
- 打开 RDB,创建
sync_tasks表。 - 调用
setDistributedTables([TABLE_TASK])把表设置为分布式表。 - 写入一条本地任务。
- 填写远端
deviceId后调用sync触发 PUSH 同步。 - 尝试调用
obtainDistributedTableName和remoteQuery查看远端数据。 - 用半模态展示数据库名、表名、同步反馈、远端查询结果。
单机环境可以验证建表、写入、注册分布式表和页面流程。真正跨设备同步需要可信组网、权限、同一应用环境和真机验证。
容易踩坑的地方
第一,setDistributedTables 不是建表语句。表要先存在,再设置为分布式表。
第二,sync 需要 RdbPredicates。如果只想同步指定设备,可以用 predicates.inDevices([deviceId]);如果要同步所有已连接设备,可以用 inAllDevices()。
第三,多设备协同表模式下,远端数据不是直接写回本地普通表。官方文档说明需要通过 obtainDistributedTableName 获取远端设备对应的分布式表名。
欢迎评论区一起分享~
你们会把哪类结构化数据做跨设备同步?任务列表、客户记录、离线表单,还是设备间共享的业务缓存?如果要处理冲突,你会更想用多设备协同表,还是 API 23 之后的单版本表?
完整示例代码
文件位置:ArkDataRdbDeviceSyncDemo.ets
import { abilityAccessCtrl, common, Permissions } from '@kit.AbilityKit';
import { relationalStore } from '@kit.ArkData';
import { promptAction, router } from '@kit.ArkUI';
const DB_NAME: string = 'rdb_device_sync_demo.db';
const TABLE_TASK: string = 'sync_tasks';
const DISTRIBUTED_PERMISSION: Permissions = 'ohos.permission.DISTRIBUTED_DATASYNC';
class RdbSyncTaskItem {
id: number = 0;
title: string = '';
ownerDevice: string = '';
statusText: string = '';
updatedAt: string = '';
constructor(id: number, title: string, ownerDevice: string, statusText: string, updatedAt: string) {
this.id = id;
this.title = title;
this.ownerDevice = ownerDevice;
this.statusText = statusText;
this.updatedAt = updatedAt;
}
}
class RdbSyncInfoItem {
label: string = '';
value: string = '';
constructor(label: string, value: string) {
this.label = label;
this.value = value;
}
}
@Entry
@Component
struct ArkDataRdbDeviceSyncDemo {
@State taskTitle: string = '同步会议纪要';
@State ownerDevice: string = 'phone';
@State remoteDeviceId: string = '';
@State tips: string = '先把普通 RDB 表设置为分布式表,再按设备 ID 触发同步。';
@State syncTips: string = '单机可验证建表、写入和订阅;跨设备效果需要可信组网。';
@State changeTips: string = '尚未收到远端数据变化通知。';
@State tasks: RdbSyncTaskItem[] = [];
@State infoItems: RdbSyncInfoItem[] = [];
@State showResultSheet: boolean = false;
private store: relationalStore.RdbStore | undefined = undefined;
private dataChangeObserver: ((devices: string[]) => void) | undefined = undefined;
private distributedTableName: string = '需要真实 deviceId 后获取';
private remoteQueryText: string = '尚未查询远端表';
private distributedReady: boolean = false;
aboutToAppear(): void {
this.preparePage();
}
aboutToDisappear(): void {
const store = this.store;
const observer = this.dataChangeObserver;
if (store !== undefined && observer !== undefined) {
store.off('dataChange', relationalStore.SubscribeType.SUBSCRIBE_TYPE_REMOTE, observer);
this.dataChangeObserver = undefined;
}
}
private async preparePage(): Promise<void> {
await this.loadTasks('初始化页面');
}
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_TASK} (` +
'id INTEGER PRIMARY KEY AUTOINCREMENT, ' +
'title TEXT NOT NULL, ' +
'owner_device TEXT NOT NULL, ' +
'status_text TEXT NOT NULL, ' +
'updated_at TEXT NOT NULL)'
);
this.store = store;
return store;
}
private async requestDistributedPermission(): Promise<boolean> {
try {
const context = getContext(this) as common.UIAbilityContext;
const atManager = abilityAccessCtrl.createAtManager();
const result = await atManager.requestPermissionsFromUser(context, [DISTRIBUTED_PERMISSION]);
const granted = result.authResults.length > 0 &&
result.authResults[0] === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED;
if (!granted) {
this.syncTips = '未获得分布式数据同步权限,本地读写仍可继续。';
this.updateInfoItems('权限未授权');
}
return granted;
} catch (err) {
this.syncTips = '申请分布式数据同步权限失败,本地读写仍可继续。';
this.updateInfoItems('权限申请失败');
console.error('RDB distributed permission request failed');
return false;
}
}
private async ensureDistributedReady(): Promise<boolean> {
if (this.distributedReady) {
return true;
}
const granted = await this.requestDistributedPermission();
if (!granted) {
promptAction.showToast({ message: '未获得分布式权限' });
return false;
}
try {
const store = await this.getStore();
await store.setDistributedTables([TABLE_TASK]);
this.distributedReady = true;
this.syncTips = '分布式表已设置,真机可信组网后可同步。';
this.subscribeRemoteChanges();
this.updateInfoItems('设置分布式表');
return true;
} catch (err) {
this.syncTips = '设置分布式表失败,请确认分布式环境和权限。';
this.updateInfoItems('设置分布式表失败');
console.error('RDB set distributed tables failed');
return false;
}
}
private subscribeRemoteChanges(): void {
if (this.dataChangeObserver !== undefined || this.store === undefined) {
return;
}
const observer = (devices: string[]) => {
this.changeTips = devices.length > 0 ?
`收到远端变化通知,设备数:${devices.length}` :
'收到远端变化通知,但设备列表为空';
this.loadTasks('远端数据变化后刷新');
};
this.dataChangeObserver = observer;
this.store.on('dataChange', relationalStore.SubscribeType.SUBSCRIBE_TYPE_REMOTE, observer);
}
private updateInfoItems(action: string): void {
this.infoItems = [
new RdbSyncInfoItem('数据库名称', DB_NAME),
new RdbSyncInfoItem('本地表名', TABLE_TASK),
new RdbSyncInfoItem('分布式表状态', this.distributedReady ? '已设置为分布式表' : '未设置,按钮触发时申请权限'),
new RdbSyncInfoItem('最近动作', action),
new RdbSyncInfoItem('当前记录数', `${this.tasks.length}`),
new RdbSyncInfoItem('远端设备 ID', this.remoteDeviceId.length > 0 ? this.remoteDeviceId : '未填写'),
new RdbSyncInfoItem('远端分布式表名', this.distributedTableName),
new RdbSyncInfoItem('同步反馈', this.syncTips),
new RdbSyncInfoItem('数据变化监听', this.changeTips),
new RdbSyncInfoItem('远端查询结果', this.remoteQueryText)
];
}
private async loadTasks(action: string = '读取本地表'): Promise<void> {
try {
const store = await this.getStore();
const resultSet = await store.querySql(
`SELECT id, title, owner_device, status_text, updated_at FROM ${TABLE_TASK} ORDER BY id DESC`
);
const rows: RdbSyncTaskItem[] = [];
const idIndex = resultSet.getColumnIndex('id');
const titleIndex = resultSet.getColumnIndex('title');
const ownerIndex = resultSet.getColumnIndex('owner_device');
const statusIndex = resultSet.getColumnIndex('status_text');
const updatedAtIndex = resultSet.getColumnIndex('updated_at');
while (resultSet.goToNextRow()) {
rows.push(new RdbSyncTaskItem(
resultSet.getLong(idIndex),
resultSet.getString(titleIndex),
resultSet.getString(ownerIndex),
resultSet.getString(statusIndex),
resultSet.getString(updatedAtIndex)
));
}
resultSet.close();
this.tasks = rows;
this.updateInfoItems(action);
} catch (err) {
this.tips = '读取失败,请查看日志。';
promptAction.showToast({ message: '读取失败' });
console.error('RDB device sync load failed');
}
}
private async saveTask(): Promise<void> {
try {
const store = await this.getStore();
const title = this.taskTitle.length > 0 ? this.taskTitle : '未命名同步任务';
const now = new Date().toString();
const bucket: relationalStore.ValuesBucket = {
title: title,
owner_device: this.ownerDevice.length > 0 ? this.ownerDevice : 'local',
status_text: 'local_saved',
updated_at: now
};
await store.insert(TABLE_TASK, bucket);
this.taskTitle = title;
this.tips = '保存成功:数据已写入本地 RDB 表。同步按钮会再设置分布式表。';
await this.loadTasks('写入本地数据');
promptAction.showToast({ message: '同步任务已保存' });
} catch (err) {
this.tips = '保存失败,请查看日志。';
promptAction.showToast({ message: '保存失败' });
console.error('RDB device sync insert failed');
}
}
private buildSyncPredicates(deviceId: string): relationalStore.RdbPredicates {
const predicates = new relationalStore.RdbPredicates(TABLE_TASK);
if (deviceId.length > 0) {
predicates.inDevices([deviceId]);
} else {
predicates.inAllDevices();
}
return predicates;
}
private async syncTasks(): Promise<void> {
try {
const ready = await this.ensureDistributedReady();
if (!ready) {
return;
}
const store = await this.getStore();
const deviceId = this.remoteDeviceId.trim();
const predicates = this.buildSyncPredicates(deviceId);
const result = await store.sync(relationalStore.SyncMode.SYNC_MODE_PUSH, predicates);
this.syncTips = `已触发 PUSH 同步,返回设备状态数:${result.length}`;
await this.loadTasks('触发 RDB 跨设备同步');
promptAction.showToast({ message: '已触发 RDB 同步' });
} catch (err) {
this.syncTips = '同步触发失败,请确认权限、可信组网和设备 ID。';
this.updateInfoItems('同步失败');
promptAction.showToast({ message: '同步失败' });
console.error('RDB device sync failed');
}
}
private async queryRemoteTable(): Promise<void> {
const deviceId = this.remoteDeviceId.trim();
if (deviceId.length === 0) {
this.remoteQueryText = '请先填写远端设备 ID。';
this.updateInfoItems('远端查询参数缺失');
promptAction.showToast({ message: '请填写设备 ID' });
return;
}
try {
const ready = await this.ensureDistributedReady();
if (!ready) {
return;
}
const store = await this.getStore();
this.distributedTableName = await store.obtainDistributedTableName(deviceId, TABLE_TASK);
const predicates = new relationalStore.RdbPredicates(TABLE_TASK);
const resultSet = await store.remoteQuery(deviceId, TABLE_TASK, predicates, []);
this.remoteQueryText = `远端查询成功,记录数:${resultSet.rowCount}`;
resultSet.close();
this.updateInfoItems('查询远端数据');
promptAction.showToast({ message: '远端查询完成' });
} catch (err) {
this.remoteQueryText = '远端查询失败,请确认设备在线并已组网。';
this.updateInfoItems('远端查询失败');
promptAction.showToast({ message: '远端查询失败' });
console.error('RDB remote query failed');
}
}
private async clearTasks(): Promise<void> {
try {
const store = await this.getStore();
await store.executeSql(`DELETE FROM ${TABLE_TASK}`);
this.tips = '本地表数据已清空。';
await this.loadTasks('清空本地表');
promptAction.showToast({ message: '已清空' });
} catch (err) {
this.tips = '清空失败,请查看日志。';
promptAction.showToast({ message: '清空失败' });
console.error('RDB device sync clear failed');
}
}
private async showSyncInfo(): Promise<void> {
await this.loadTasks('查看 RDB 同步信息');
this.showResultSheet = true;
}
@Builder
private syncInfoSheet() {
Column({ space: 14 }) {
Text('RDB 跨设备同步信息')
.width('100%')
.fontSize(22)
.fontWeight(700)
.fontColor('#0F172A')
Text('这里展示当前 Demo 的数据库、分布式表、设备 ID、同步反馈和远端查询结果。列表只展示当前数据库信息,不做 LazyForEach 懒加载渲染处理。')
.width('100%')
.fontSize(13)
.fontColor('#64748B')
Column({ space: 8 }) {
ForEach(this.infoItems, (item: RdbSyncInfoItem) => {
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: RdbSyncInfoItem) => item.label)
}
.width('100%')
Button('关闭')
.width('100%')
.height(44)
.fontSize(16)
.backgroundColor('#1D4ED8')
.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('#1D4ED8')
.onClick(() => {
router.back();
})
Blank()
}
.width('100%')
Text('RDB 跨设备同步')
.width('100%')
.fontSize(28)
.fontWeight(700)
.fontColor('#182431')
Text('把普通 RDB 表设置为分布式表,写入本地数据后再按设备 ID 触发同步。')
.width('100%')
.fontSize(14)
.fontColor('#56616F')
TextInput({ placeholder: '输入任务标题', text: this.taskTitle })
.height(44)
.fontSize(16)
.backgroundColor('#EFF6FF')
.fontColor('#0F172A')
.onChange((value: string) => {
this.taskTitle = value;
})
TextInput({ placeholder: '输入本机标识,比如 phone', text: this.ownerDevice })
.height(44)
.fontSize(16)
.backgroundColor('#EFF6FF')
.fontColor('#0F172A')
.onChange((value: string) => {
this.ownerDevice = value;
})
TextInput({ placeholder: '真机组网后填写远端 deviceId', text: this.remoteDeviceId })
.height(44)
.fontSize(16)
.backgroundColor('#DBEAFE')
.fontColor('#0F172A')
.onChange((value: string) => {
this.remoteDeviceId = value;
this.updateInfoItems('更新设备 ID');
})
Row({ space: 10 }) {
Button('保存')
.layoutWeight(1)
.height(44)
.fontSize(15)
.backgroundColor('#1D4ED8')
.onClick(() => {
this.saveTask();
})
Button('同步')
.layoutWeight(1)
.height(44)
.fontSize(15)
.fontColor('#FFFFFF')
.backgroundColor('#0F766E')
.onClick(() => {
this.syncTasks();
})
Button('远端查询')
.layoutWeight(1)
.height(44)
.fontSize(15)
.fontColor('#0F172A')
.backgroundColor('#BFDBFE')
.onClick(() => {
this.queryRemoteTable();
})
}
.width('100%')
Row({ space: 10 }) {
Button('查看')
.layoutWeight(1)
.height(44)
.fontSize(15)
.fontColor('#0F172A')
.backgroundColor('#DBEAFE')
.onClick(() => {
this.showSyncInfo();
})
Button('清空')
.layoutWeight(1)
.height(44)
.fontSize(15)
.fontColor('#0F172A')
.backgroundColor('#CBD5E1')
.onClick(() => {
this.clearTasks();
})
}
.width('100%')
Column({ space: 8 }) {
ForEach(this.tasks, (item: RdbSyncTaskItem) => {
Column({ space: 4 }) {
Text(item.title)
.width('100%')
.fontSize(17)
.fontWeight(700)
.fontColor('#0F172A')
Text(`id=${item.id} | owner=${item.ownerDevice} | status=${item.statusText}`)
.width('100%')
.fontSize(12)
.fontColor('#64748B')
Text(item.updatedAt)
.width('100%')
.fontSize(12)
.fontColor('#94A3B8')
}
.width('100%')
.padding(14)
.backgroundColor('#F8FAFC')
.borderRadius(8)
}, (item: RdbSyncTaskItem) => `${item.id}`)
}
.width('100%')
Text(this.tips)
.width('100%')
.fontSize(13)
.fontColor('#64748B')
Text(this.syncTips)
.width('100%')
.fontSize(13)
.fontColor('#0F766E')
Text(this.changeTips)
.width('100%')
.fontSize(13)
.fontColor('#1D4ED8')
}
.width('100%')
.padding(24)
}
.width('100%')
.height('100%')
.backgroundColor('#FFFFFF')
.bindSheet(this.showResultSheet, this.syncInfoSheet(), {
height: SheetSize.MEDIUM,
dragBar: true,
showClose: true,
onDisappear: () => {
this.showResultSheet = false;
}
})
}
}
- 点赞
- 收藏
- 关注作者
评论(0)