HarmonyOS 新手入门:ArkData 关系型数据库跨设备同步,分布式表不是普通表

举报
蓝瘦的蜕变 发表于 2026/07/03 10:26:44 2026/07/03
【摘要】 RDB 前面已经写过建表、CRUD、升级、事务和 Sendable。再往多设备场景走,就会遇到一个容易混的点:关系型数据库也能做跨设备同步,但它不是“把普通表自动复制到另一台设备”。 官方文档《关系型数据库跨设备数据同步 (ArkTS)》里讲得很明确:先把表设置为分布式表,再通过同步接口在可信设备之间同步数据。API

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 同步。
  • 尝试调用 obtainDistributedTableNameremoteQuery 查看远端数据。
  • 用半模态展示数据库名、表名、同步反馈、远端查询结果。

单机环境可以验证建表、写入、注册分布式表和页面流程。真正跨设备同步需要可信组网、权限、同一应用环境和真机验证。

容易踩坑的地方

第一,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;
      }
    })
  }
}
【版权声明】本文为华为云社区用户原创内容,未经允许不得转载,如需转载请自行联系原作者进行授权。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

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

全部回复

上滑加载中

设置昵称

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

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

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