鸿蒙开发——关系型数据库的基本使用与跨设备同步

200 阅读10分钟

1、关系型数据库的基本使用

1.1、 基本介绍

关系型数据库基于SQLite组件,适用于存储包含复杂关系数据的场景,比如一个班级的学生信息,需要包括姓名、学号、各科成绩等,又或者公司的雇员信息,需要包括姓名、工号、职位等,由于数据之间有较强的对应关系,复杂程度比键值型数据更高,此时需要使用关系型数据库来持久化保存数据。

大数据量场景下查询数据可能会导致耗时长甚至应用卡死,有建议如下:

  • 单次查询数据量不超过5000条。

  • 在TaskPool中查询。

  • 拼接SQL语句尽量简洁。

  • 合理地分批次查询。

1.2、 一些注意事项

  • 系统默认日志方式是WAL(Write Ahead Log)模式,系统默认落盘方式是FULL模式。

  • 数据库中有4个读连接和1个写连接,线程获取到空闲读连接时,即可进行读取操作。当没有空闲读连接且有空闲写连接时,会将写连接当做读连接来使用。

  • 为保证数据的准确性,数据库同一时间只能支持一个写操作。

  • 当应用被卸载完成后,设备上的相关数据库文件及临时文件会被自动清除。

  • ArkTS侧支持的基本数据类型:number、string、二进制类型数据、boolean。

  • 为保证插入并读取数据成功,建议一条数据不要超过2M。超出该大小,插入成功,读取失败。

1.3、 接口说明

以下是关系型数据库持久化功能的相关接口,大部分为异步接口。异步接口均有callback和Promise两种返回形式,下面均以callback形式为例。

// 获得一个RdbStore,操作关系型数据库,用户可以根据自己的需求配置RdbStore的参数,然后通过RdbStore调用相关接口可以执行相关的数据操作。
getRdbStore(context: Context, config: StoreConfig, callback: AsyncCallback<RdbStore>): void

// 执行包含指定参数但不返回值的SQL语句。
executeSql(sqlstring, bindArgs: Array<ValueType>, callback: AsyncCallback<void>):void

// 向目标表中插入一行数据。
insert(tablestring, values: ValuesBucket, callback: AsyncCallback<number>):void
// 根据predicates的指定实例对象更新数据库中的数据。
update(values: ValuesBucket, predicates: RdbPredicates, callback: AsyncCallback<number>):void
// 根据predicates的指定实例对象从数据库中删除数据。
delete(predicates: RdbPredicates, callback: AsyncCallback<number>):void
// 根据指定条件查询数据库中的数据。
query(predicates: RdbPredicates, columns: Array<string>, callback: AsyncCallback<ResultSet>):void

// 删除数据库。
deleteRdbStore(context: Context, namestring, callback: AsyncCallback<void>): void

1.4、数据库的基本操作

👉🏻 获取RdbStore实例

RdbStore是数据库操作的基本对象,建库、建表、数据库的增/删/改/查操作都依赖于RdbStore对象。我们可以通过下面的接口获得一个RdbStore对象。

import { relationalStore } from '@kit.ArkData'; // 导入模块
// 获得一个RdbStore,操作关系型数据库,用户可以根据自己的需求配置RdbStore的参数,然后通过RdbStore调用相关接口可以执行相关的数据操作。
relationalStore.getRdbStore(context: Context, config: StoreConfig, callback: AsyncCallback<RdbStore>): void

说明

  • 应用创建的数据库与其上下文(Context)有关,即使使用同样的数据库名称,但不同的应用上下文,会产生多个数据库,例如每个UIAbility都有各自的上下文。

  • 当应用首次获取数据库(调用getRdbStore)后,在应用沙箱内会产生对应的数据库文件。使用数据库的过程中,在与数据库文件相同的目录下可能会产生以-wal和-shm结尾的临时文件。此时若开发者希望移动数据库文件到其它地方使用查看,则需要同时移动这些临时文件,当应用被卸载完成后,其在设备上产生的数据库文件及临时文件也会被移除。

获取RdbStore实例,并判断数据库版本号,进行数据库升级/降级操作的示例代码如下:

import { relationalStore } from '@kit.ArkData'; // 导入模块
import { UIAbility } from '@kit.AbilityKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { window } from '@kit.ArkUI';

// 此处示例在Ability中实现,使用者也可以在其他合理场景中使用
class EntryAbility extends UIAbility {
  onWindowStageCreate(windowStage: window.WindowStage) {
    const STORE_CONFIG :relationalStore.StoreConfig= {
      name: 'RdbTest.db', // 数据库文件名
      securityLevel: relationalStore.SecurityLevel.S3, // 数据库安全级别
      encrypt: false, // 可选参数,指定数据库是否加密,默认不加密
      customDir: 'customDir/subCustomDir', // 可选参数,数据库自定义路径。数据库将在如下的目录结构中被创建:context.databaseDir + '/rdb/' + customDir,其中context.databaseDir是应用沙箱对应的路径,'/rdb/'表示创建的是关系型数据库,customDir表示自定义的路径。当此参数不填时,默认在本应用沙箱目录下创建RdbStore实例。
      isReadOnly: false // 可选参数,指定数据库是否以只读方式打开。该参数默认为false,表示数据库可读可写。该参数为true时,只允许从数据库读取数据,不允许对数据库进行写操作,否则会返回错误码801。
    };

    // 判断数据库版本,如果不匹配则需进行升降级操作
    // 假设当前数据库版本为3,表结构:EMPLOYEE (NAME, AGE, SALARY, CODES, IDENTITY)
    const SQL_CREATE_TABLE = 'CREATE TABLE IF NOT EXISTS EMPLOYEE (ID INTEGER PRIMARY KEY AUTOINCREMENT, NAME TEXT NOT NULL, AGE INTEGER, SALARY REAL, CODES BLOB, IDENTITY UNLIMITED INT)'; // 建表Sql语句, IDENTITY为bigint类型,sql中指定类型为UNLIMITED INT

    relationalStore.getRdbStore(this.context, STORE_CONFIG, (err, store) => {
      if (err) {
        console.error(`Failed to get RdbStore. Code:${err.code}, message:${err.message}`);
        return;
      }
      console.info('Succeeded in getting RdbStore.');

      // 当数据库创建时,数据库默认版本为0
      if (store.version === 0) {
        store.executeSql(SQL_CREATE_TABLE); // 创建数据表
        // 设置数据库的版本,入参为大于0的整数
        store.version = 3;
      }

      // 如果数据库版本不为0且和当前数据库版本不匹配,需要进行升降级操作
      // 当数据库存在并假定版本为1时,例应用从某一版本升级到当前版本,数据库需要从1版本升级到2版本
      if (store.version === 1) {
        // version = 1:表结构:EMPLOYEE (NAME, SALARY, CODES, ADDRESS) => version = 2:表结构:EMPLOYEE (NAME, AGE, SALARY, CODES, ADDRESS)
        (store as relationalStore.RdbStore).executeSql('ALTER TABLE EMPLOYEE ADD COLUMN AGE INTEGER');
        store.version = 2;
      }

      // 当数据库存在并假定版本为2时,例应用从某一版本升级到当前版本,数据库需要从2版本升级到3版本
      if (store.version === 2) {
        // version = 2:表结构:EMPLOYEE (NAME, AGE, SALARY, CODES, ADDRESS) => version = 3:表结构:EMPLOYEE (NAME, AGE, SALARY, CODES)
        (store as relationalStore.RdbStore).executeSql('ALTER TABLE EMPLOYEE DROP COLUMN ADDRESS TEXT');
        store.version = 3;
      }
    });

    // 请确保获取到RdbStore实例后,再进行数据库的增、删、改、查等操作
  }
}

👉🏻 通过RdbStore插入数据

插入数据的接口如下:

// 向目标表中插入一行数据。
insert(tablestring, values: ValuesBucket, callback: AsyncCallback<number>):void

一个插入数据的示例如下:

let store: relationalStore.RdbStore | undefined = undefined;

let value1 = 'Lisa';
let value2 = 18;
let value3 = 100.5;
let value4 = new Uint8Array([1, 2, 3, 4, 5]);
let value5 = BigInt('15822401018187971961171');
// 以下三种方式可用
const valueBucket1: relationalStore.ValuesBucket = {
  'NAME': value1,
  'AGE': value2,
  'SALARY': value3,
  'CODES': value4,
  'IDENTITY': value5,
};
const valueBucket2: relationalStore.ValuesBucket = {
  NAME: value1,
  AGE: value2,
  SALARY: value3,
  CODES: value4,
  IDENTITY: value5,
};
const valueBucket3: relationalStore.ValuesBucket = {
  "NAME": value1,
  "AGE": value2,
  "SALARY": value3,
  "CODES": value4,
  "IDENTITY": value5,
};

if (store !== undefined) {
  (store as relationalStore.RdbStore).insert('EMPLOYEE', valueBucket1, (err: BusinessError, rowId: number) => {
    if (err) {
      console.error(`Failed to insert data. Code:${err.code}, message:${err.message}`);
      return;
    }
    console.info(`Succeeded in inserting data. rowId:${rowId}`);
  })
}

👉🏻 通过RdbStore更新和删除数据

更新和删除数据的接口定义如下:

// 根据predicates的指定实例对象更新数据库中的数据。
update(values: ValuesBucket, predicates: RdbPredicates, callback: AsyncCallback<number>):void
// 根据predicates的指定实例对象从数据库中删除数据。
delete(predicates: RdbPredicates, callback: AsyncCallback<number>):void

一个更新和删除的示例代码如下:

let value6 = 'Rose';
let value7 = 22;
let value8 = 200.5;
let value9 = new Uint8Array([1, 2, 3, 4, 5]);
let value10 = BigInt('15822401018187971967863');
// 以下三种方式可用
const valueBucket4: relationalStore.ValuesBucket = {
  'NAME': value6,
  'AGE': value7,
  'SALARY': value8,
  'CODES': value9,
  'IDENTITY': value10,
};
const valueBucket5: relationalStore.ValuesBucket = {
  NAME: value6,
  AGE: value7,
  SALARY: value8,
  CODES: value9,
  IDENTITY: value10,
};
const valueBucket6: relationalStore.ValuesBucket = {
  "NAME": value6,
  "AGE": value7,
  "SALARY": value8,
  "CODES": value9,
  "IDENTITY": value10,
};

// 修改数据
let predicates1 = new relationalStore.RdbPredicates('EMPLOYEE'); // 创建表'EMPLOYEE'的predicates
predicates1.equalTo('NAME', 'Lisa'); // 匹配表'EMPLOYEE'中'NAME'为'Lisa'的字段
if (store !== undefined) {
  (store as relationalStore.RdbStore).update(valueBucket4, predicates1, (err: BusinessError, rows: number) => {
    if (err) {
      console.error(`Failed to update data. Code:${err.code}, message:${err.message}`);
     return;
   }
   console.info(`Succeeded in updating data. row count: ${rows}`);
  })
}

// 删除数据
predicates1 = new relationalStore.RdbPredicates('EMPLOYEE');
predicates1.equalTo('NAME', 'Lisa');
if (store !== undefined) {
  (store as relationalStore.RdbStore).delete(predicates1, (err: BusinessError, rows: number) => {
    if (err) {
      console.error(`Failed to delete data. Code:${err.code}, message:${err.message}`);
      return;
    }
    console.info(`Delete rows: ${rows}`);
  })
}

👉🏻 通过RdbStore查询数据

查询数据接口定义如下:

// 根据指定条件查询数据库中的数据。
query(predicates: RdbPredicates, columns: Array<string>, callback: AsyncCallback<ResultSet>):void

一个查询数据的实例代码如下:

let predicates2 = new relationalStore.RdbPredicates('EMPLOYEE');
predicates2.equalTo('NAME', 'Rose');
if (store !== undefined) {
  (store as relationalStore.RdbStore).query(predicates2, ['ID', 'NAME', 'AGE', 'SALARY', 'IDENTITY'], (err: BusinessError, resultSet) => {
    if (err) {
      console.error(`Failed to query data. Code:${err.code}, message:${err.message}`);
      return;
    }
    console.info(`ResultSet column names: ${resultSet.columnNames}, column count: ${resultSet.columnCount}`);
    // resultSet是一个数据集合的游标,默认指向第-1个记录,有效的数据从0开始。
    while (resultSet.goToNextRow()) {
      const id = resultSet.getLong(resultSet.getColumnIndex('ID'));
      const name = resultSet.getString(resultSet.getColumnIndex('NAME'));
      const age = resultSet.getLong(resultSet.getColumnIndex('AGE'));
      const salary = resultSet.getDouble(resultSet.getColumnIndex('SALARY'));
      const identity = resultSet.getValue(resultSet.getColumnIndex('IDENTITY'));
      console.info(`id=${id}, name=${name}, age=${age}, salary=${salary}, identity=${identity}`);
    }
    // 释放数据集的内存
    resultSet.close();
  })
}
当应用完成查询数据操作,不再使用结果集(ResultSet)时,请及时调用close方法关闭结果集,释放系统为其分配的内存。

1.5、数据库的备份、恢复与删除

备份数据库代码示例如下:

import { relationalStore } from '@kit.ArkData'; // 导入模块

// ...
if (store !== undefined) {
  // "Backup.db"为备份数据库文件名,默认在RdbStore同路径下备份。也可指定路径:customDir + "backup.db"
  (store as relationalStore.RdbStore).backup("Backup.db", (err: BusinessError) => {
    if (err) {
      console.error(`Failed to backup RdbStore. Code:${err.code}, message:${err.message}`);
      return;
    }
    console.info(`Succeeded in backing up RdbStore.`);
  })
}

恢复数据库代码示例如下:

import { relationalStore } from '@kit.ArkData'; // 导入模块

// ...
if (store !== undefined) {
  (store as relationalStore.RdbStore).restore("Backup.db", (err: BusinessError) => {
    if (err) {
      console.error(`Failed to restore RdbStore. Code:${err.code}, message:${err.message}`);
      return;
    }
    console.info(`Succeeded in restoring RdbStore.`);
  })
}

调用deleteRdbStore()方法,删除数据库及数据库相关文件。示例代码如下:

import { relationalStore } from '@kit.ArkData'; // 导入模块

// ...
relationalStore.deleteRdbStore(this.context, 'RdbTest.db', (err: BusinessError) => {
 if (err) {
    console.error(`Failed to delete RdbStore. Code:${err.code}, message:${err.message}`);
    return;
  }
  console.info('Succeeded in deleting RdbStore.');
});

2、关系型数据库跨设备同步

当应用程序本地存储的关系型数据存在跨设备同步的需求时,可以将需要同步的表数据迁移到新的支持跨设备的表中,当然也可以在刚完成表创建时设置其支持跨设备。

关系型数据库跨设备数据同步,支持应用在多设备间同步存储的关系型数据。

  • 应用在数据库中新创建表后,可以设置其为分布式表。在查询远程设备数据库时,根据本地表名可以获取指定远程设备的分布式表名。

  • 设备之间同步数据,数据同步有两种方式,将数据从本地设备推送到远程设备或将数据从远程设备拉至本地设备。

每个应用程序最多支持同时打开16个关系型分布式数据库。

单个数据库最多支持注册8个订阅数据变化的回调。

使用此功能需要申请权限:

  • 需要申请ohos.permission.DISTRIBUTED_DATASYNC权限。

  • 同时需要在应用首次启动时弹窗向用户申请授权。

2.1、接口说明

以下是关系型设备协同分布式数据库跨设备数据同步功能的相关接口,大部分为异步接口。异步接口均有callback和Promise两种返回形式,下面均以callback形式为例。

// 设置分布式同步表。
setDistributedTables(tables: Array<string>, callback: AsyncCallback<void>): void

// 分布式数据同步。
sync(mode: SyncMode, predicates: RdbPredicates, callback: AsyncCallback<Array<[string, number]>>): void

// 取消/订阅分布式数据变化。
on(event: 'dataChange', type: SubscribeType, observer: Callback<Array<string>>): void  
off(event:'dataChange', type: SubscribeType, observer: Callback<Array<string>>): void  

// 根据本地数据库表名获取指定设备上的表名。
obtainDistributedTableName(devicestring, tablestring, callback: AsyncCallback<string>): void

// 根据指定条件查询远程设备数据库中的数据。
remoteQuery(devicestring, tablestring, predicates: RdbPredicates, columns: Array<string> , callback: AsyncCallback<ResultSet>): void

2.2、场景代码示例

👉🏻 创建一个分布式同步的表

示例代码如下(14 ~ 18行):

import { relationalStore } from '@kit.ArkData';
import { UIAbility } from '@kit.AbilityKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { window } from '@kit.ArkUI';

class EntryAbility extends UIAbility {
  onWindowStageCreate(windowStage: window.WindowStage) {
    const STORE_CONFIG: relationalStore.StoreConfig = {
      name: "RdbTest.db",
      securityLevel: relationalStore.SecurityLevel.S1
    };
       
    relationalStore.getRdbStore(this.context, STORE_CONFIG, (err: BusinessError, store: relationalStore.RdbStore) => {
      store.executeSql('CREATE TABLE IF NOT EXISTS EMPLOYEE (ID INTEGER PRIMARY KEY AUTOINCREMENT, NAME TEXT NOT NULL, AGE INTEGER, SALARY REAL, CODES BLOB)', (err) => {
        // 设置分布式同步表。
        store.setDistributedTables(['EMPLOYEE']);
        // 进行数据的相关操作
      })
    })
  }
}

👉🏻 分布式数据同步

使用SYNC_MODE_PUSH触发同步后,数据将从本设备向组网内的其它所有设备同步。示例代码如下:

// 构造用于同步分布式表的谓词对象
let predicates = new relationalStore.RdbPredicates('EMPLOYEE');
// 调用同步数据的接口
if(store != undefined)
{
  (store as relationalStore.RdbStore).sync(relationalStore.SyncMode.SYNC_MODE_PUSH, predicates, (err, result) => {
    // 判断数据同步是否成功
    if (err) {
      console.error(`Failed to sync data. Code:${err.code},message:${err.message}`);
      return;
    }
    console.info('Succeeded in syncing data.');
    for (let i = 0; i < result.length; i++) {
      console.info(`device:${result[i][0]},status:${result[i][1]}`);
    }
  })
}

👉🏻 分布式数据订阅

数据同步变化将触发订阅回调方法执行,回调方法的入参为发生变化的设备ID。示例代码如下:

let devices: string | undefined = undefined;
try {
  // 调用分布式数据订阅接口,注册数据库的观察者
  // 当分布式数据库中的数据发生更改时,将调用回调
  if(store != undefined) {
    (store as relationalStore.RdbStore).on('dataChange', relationalStore.SubscribeType.SUBSCRIBE_TYPE_REMOTE, (storeObserver)=>{
      if(devices != undefined){
        for (let i = 0; i < devices.length; i++) {
          console.info(`The data of device:${devices[i]} has been changed.`);
        }
      }
    });
  }
} catch (err) {
  console.error('Failed to register observer. Code:${err.code},message:${err.message}');
}
// 当前不需要订阅数据变化时,可以将其取消。
try {
  if(store != undefined) {
    (store as relationalStore.RdbStore).off('dataChange', relationalStore.SubscribeType.SUBSCRIBE_TYPE_REMOTE, (storeObserver)=>{
    });
  }
} catch (err) {
  console.error('Failed to register observer. Code:${err.code},message:${err.message}');
}

👉🏻 跨设备查询

如果数据未完成同步,或未触发数据同步,应用可以使用此接口从指定的设备上查询数据。

我们可以通过调用deviceManager.getAvailableDeviceListSync方法得到ideviceds列表

示例代码如下:

// 获取deviceIds
import { distributedDeviceManager } from '@kit.DistributedServiceKit';
import { BusinessError } from '@kit.BasicServicesKit';

let dmInstance: distributedDeviceManager.DeviceManager;
let deviceId: string | undefined = undefined ;

try {
  dmInstance = distributedDeviceManager.createDeviceManager("com.example.appdatamgrverify");
  let devices = dmInstance.getAvailableDeviceListSync();

  deviceId = devices[0].networkId;

  // 构造用于查询分布式表的谓词对象
  let predicates = new relationalStore.RdbPredicates('EMPLOYEE');
  // 调用跨设备查询接口,并返回查询结果
  if(store != undefined && deviceId != undefined) {
    (store as relationalStore.RdbStore).remoteQuery(deviceId, 'EMPLOYEE', predicates, ['ID', 'NAME', 'AGE', 'SALARY', 'CODES'],
      (err: BusinessError, resultSet: relationalStore.ResultSet) => {
        if (err) {
          console.error(`Failed to remoteQuery data. Code:${err.code},message:${err.message}`);
          return;
        }
        console.info(`ResultSet column names: ${resultSet.columnNames}, column count: ${resultSet.columnCount}`);
      }
    )
  }
} catch (err) {
  let code = (err as BusinessError).code;
  let message = (err as BusinessError).message;
  console.error("createDeviceManager errCode:" + code + ",errMessage:" + message);
}