跨设备同步实战:多设备协同表模式的联系人同步案例

分布式是鸿蒙的招牌能力。手机上改了联系人,平板上要看到;平板上加了个备忘录,手机上要同步出现——这就是跨设备数据同步。关系型数据库提供了两种同步模式:多设备协同表和单版本表。下面先用一个"联系人同步"案例把多设备协同表模式讲透,下一篇再讲单版本表。

一、案例背景:多设备联系人同步

一个通讯录应用,用户在手机上添加/修改联系人,平板上要同步看到。需求:

  • 手机端新增联系人 → 推送到平板
  • 平板端能查询手机同步过来的联系人
  • 平板端数据变化时,手机端能收到通知

这正是多设备协同表模式的典型场景。

二、多设备协同表模式的运作机制

先理解机制,再写代码。多设备协同表模式的核心是数据隔离存储

  • 每个设备的数据存在独立的分布式表中,不直接写入本地表
  • 分布式表名 = 对端设备 DeviceID + 原表名
  • 设备 A 收到设备 B 同步过来的数据,会写入"B 的分布式表",通过 obtainDistributedTableName 拿到表名再查
  • 不支持修改其他设备同步过来的数据(保障一致性和同步稳定性)

这个"只读对端数据"的限制是设计上的取舍——避免双向修改导致冲突。如果需要双向修改,得用下一篇讲的单版本表模式。

三、第一步:导入模块和申请权限

import { relationalStore } from '@kit.ArkData';
import { BusinessError } from '@kit.BasicServicesKit';
import { distributedDeviceManager } from '@kit.DistributedServiceKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { common } from '@kit.AbilityKit';
import { UIContext } from '@kit.ArkUI';

const DOMAIN = 0x0000;

跨设备同步需要 ohos.permission.DISTRIBUTED_DATASYNC 权限,要在 module.json5 声明,并在应用首次启动时弹窗向用户申请授权。

四、第二步:建库建表 + 设置分布式表

let store: relationalStore.RdbStore | undefined = undefined;

async function initDistributedDb(context: common.UIAbilityContext) {
  const STORE_CONFIG: relationalStore.StoreConfig = {
    name: 'RdbTest.db',
    securityLevel: relationalStore.SecurityLevel.S3
  };

  relationalStore.getRdbStore(context, STORE_CONFIG).then(async (rdbStore: relationalStore.RdbStore) => {
    store = rdbStore;
    // 建联系人表
    await store.executeSql(
      'CREATE TABLE IF NOT EXISTS EMPLOYEE (ID INTEGER PRIMARY KEY AUTOINCREMENT, NAME TEXT NOT NULL, AGE INTEGER, SALARY REAL, CODES BLOB)'
    );
    // 把表设置为分布式表(默认多设备协同表模式)
    await store.setDistributedTables(['EMPLOYEE']);
  }).catch((err: BusinessError) => {
    hilog.error(DOMAIN, 'sync', `获取 RdbStore 失败: ${err.message}`);
  });
}

setDistributedTables 是关键——调用后,这张表就具备了跨设备同步能力。默认采用多设备协同表模式。

五、第三步:订阅其他设备的数据变化

平板要实时知道手机改了什么,用订阅:

async function subscribeRemoteChange() {
  if (!store) return;
  try {
    // 查询组网内的设备列表
    const deviceManager = distributedDeviceManager.createDeviceManager('com.example.contactSync');
    const deviceList = deviceManager.getAvailableDeviceListSync();
    const devices: string[] = [];
    deviceList.forEach(item => {
      if (item.networkId) devices.push(item.networkId);
    });

    // 订阅远程设备数据变化
    store.on('dataChange', relationalStore.SubscribeType.SUBSCRIBE_TYPE_REMOTE, async (changedDevices) => {
      for (let i = 0; i < changedDevices.length; i++) {
        let device = changedDevices[i];
        if (!store) return;
        hilog.info(DOMAIN, 'sync', `设备 ${device} 数据变化`);
        
        // 获取该设备对应的分布式表名
        const distributedTableName = await store.obtainDistributedTableName(device, 'EMPLOYEE');
        
        // 查询该设备同步过来的数据
        const predicates = new relationalStore.RdbPredicates(distributedTableName);
        const resultSet = await store.query(predicates);
        hilog.info(DOMAIN, 'sync', `设备 ${device} 同步了 ${resultSet.rowCount} 条联系人`);
        resultSet.close();
      }
    });
  } catch (err) {
    hilog.error(DOMAIN, 'sync', `订阅失败: ${(err as BusinessError).message}`);
  }
}

关键接口

  • on('dataChange', SUBSCRIBE_TYPE_REMOTE, callback):订阅远程设备数据变化
  • obtainDistributedTableName(device, 'EMPLOYEE'):拿到对端设备的分布式表名(拼了 DeviceID)
  • 用这个表名查谓词,就能读到对端同步过来的数据

六、第四步:推送本地数据到其他设备

手机端新增联系人后,推送到平板:

async function pushContactToRemote() {
  if (!store) return;
  
  // 1. 本地插入新联系人
  const ret = store.insertSync('EMPLOYEE', {
    name: '张三',
    age: 28,
    salary: 15000
  });
  hilog.info(DOMAIN, 'sync', `本地插入成功,rowId: ${ret}`);

  // 2. 查询组网设备
  const deviceManager = distributedDeviceManager.createDeviceManager('com.example.contactSync');
  const deviceList = deviceManager.getAvailableDeviceListSync();
  const syncTarget: string[] = [];
  deviceList.forEach(item => {
    if (item.networkId) syncTarget.push(item.networkId);
  });

  if (syncTarget.length === 0) {
    hilog.error(DOMAIN, 'sync', '没有可同步的设备');
    return;
  }

  // 3. 推送数据到其他设备
  const predicates = new relationalStore.RdbPredicates('EMPLOYEE');
  predicates.inDevices(syncTarget);  // 指定目标设备
  
  try {
    const result = await store.sync(relationalStore.SyncMode.SYNC_MODE_PUSH, predicates);
    hilog.info(DOMAIN, 'sync', '推送完成');
    // result 是每个设备的同步结果
    for (let i = 0; i < result.length; i++) {
      const deviceId = result[i][0];
      const syncResult = result[i][1];
      if (syncResult === 0) {
        hilog.info(DOMAIN, 'sync', `设备 ${deviceId} 同步成功`);
      } else {
        hilog.error(DOMAIN, 'sync', `设备 ${deviceId} 同步失败,状态: ${syncResult}`);
      }
    }
  } catch (e) {
    hilog.error(DOMAIN, 'sync', `推送失败: ${(e as BusinessError).message}`);
  }
}

sync 接口两种模式

  • SYNC_MODE_PUSH:推送本地变更到其他设备
  • SYNC_MODE_PULL:拉取其他设备的变更到本地

七、第五步:拉取其他设备的数据

async function pullContactFromRemote() {
  if (!store) return;
  
  const deviceManager = distributedDeviceManager.createDeviceManager('com.example.contactSync');
  const deviceList = deviceManager.getAvailableDeviceListSync();
  const syncTarget: string[] = [];
  deviceList.forEach(item => {
    if (item.networkId) syncTarget.push(item.networkId);
  });

  const predicates = new relationalStore.RdbPredicates('EMPLOYEE');
  predicates.inDevices(syncTarget);
  
  const result = await store.sync(relationalStore.SyncMode.SYNC_MODE_PULL, predicates);
  hilog.info(DOMAIN, 'sync', '拉取完成');
}

八、第六步:远程查询(未同步时也能查)

数据还没同步完,或者没触发同步时,想直接查远程设备的数据,用 remoteQuery

async function remoteQuery() {
  if (!store) return;
  
  const deviceManager = distributedDeviceManager.createDeviceManager('com.example.contactSync');
  const deviceList = deviceManager.getAvailableDeviceListSync();
  const devices: string[] = [];
  deviceList.forEach(item => {
    if (item.networkId) devices.push(item.networkId);
  });

  if (devices.length === 0) return;

  const predicates = new relationalStore.RdbPredicates('EMPLOYEE');
  try {
    // 直接查询远程设备上的数据,不需要先同步
    const resultSet = await store.remoteQuery(
      devices[0], 'EMPLOYEE', predicates, ['ID', 'NAME', 'AGE', 'SALARY']
    );
    hilog.info(DOMAIN, 'sync', `远程查询结果: ${resultSet.rowCount}`);
    resultSet.close();
  } catch (e) {
    hilog.error(DOMAIN, 'sync', `远程查询失败: ${(e as BusinessError).message}`);
  }
}

remoteQuery 适合"偶尔看一眼对端数据"的场景,不用维护同步状态。

九、数据同步的访问控制

官方提醒:数据只允许向数据安全标签不高于对端设备安全等级的设备同步

也就是说,如果数据安全级别是 S3,对端设备安全等级只有 S2,数据不会同步过去。这个机制保证了敏感数据不会流向低安全等级的设备。

设计同步时要注意:

  • 别给要同步的数据设太高的安全级别(同步不出去)
  • 也别设太低(敏感数据泄露)

十、约束限制速查

约束说明
同时打开数据库数每应用最多 16 个
订阅回调数单库最多 8 个
复合键表不能设为分布式表
端端 vs 端云同一表不能同时配置,且不可切换
同库同步机制所有分布式表必须同一种机制,不可切换
多设备协同表不支持修改对端同步过来的数据
多设备协同表不支持设置 schema

十一、多设备协同表的适用场景

总结一下这种模式适合什么:

  • 单向数据同步:A 设备产生数据,B 设备只读
  • 数据展示型场景:联系人、日程、备忘录的"多端可见"
  • 不涉及冲突处理:各设备数据隔离,不会互相覆盖

不适合:

  • 多端都能改同一份数据:需要单版本表模式
  • 需要复杂冲突解决:需要单版本表模式 + schema 配置

十二、总结一下下

  1. 数据隔离存储:各设备数据在独立分布式表,通过 obtainDistributedTableName 查询
  2. 对端数据只读:不能修改其他设备同步过来的数据
  3. sync 两种模式:PUSH 推送本地变更,PULL 拉取对端变更
  4. remoteQuery 直查:不同步也能查远程数据
  5. 安全级别限制:数据只能流向安全等级足够的设备
  6. 权限要申请:DISTRIBUTED_DATASYNC 权限 + 用户授权
Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐