[鸿蒙开发] 14 - HarmonyOS应用中如何实现数据持久化存储

1,536 阅读7分钟

1. 介绍

数据存储属于ArkData(方舟数据管理)的一部分。ArkData除了提供数据存储的能力,也提供了数据管理和数据同步能力,比如联系人应用数据可以保存到数据库中,提供数据库的安全、可靠以及共享访问等管理机制,也支持与手表同步联系人信息。

我们这里先了解下数据存储,应用创建的数据库,存储的数据都是会保存到应用沙盒中,当应用卸载时,数据库也会自动删除。

在ArkData中,根据数据特点,数据持久化存储的方式有以下三种:

  • 用户首选项(Preferences)

    • 类似iOS的UserDefaults或者Android的SharedPreference,提供了轻量级配置数据的持久能力;
    • 支持订阅数据变化的通知能力;
    • 不支持分布式同步,常用于保存应用配置信息、用户偏好设置等;
    • 会将文本中的数据全量加载到内存中,访问快,效率高,但不适合存储大量数据;
  • 键值型数据库(KV-Store):

    • 提供了键值型数据库的读写、加密、手动备份以及订阅通知的能力;
    • 应用需要使用键值型数据库的分布式能力时,KV-Store会将同步请求发送给DatamgrService由其完成跨设备数据同步;
  • 关系型数据库(RelationStore):

    • 提供了关系型数据库的读写、加密、手动备份以及订阅通知的能力;
    • 如果需要使用关系型数据库的分布式能力时,RelationalStore部件会将同步请求发送给DatamgrService由其完成跨设备数据同步
    • 比如如果要在App内部存储复杂的数据结构,可以使用关系型数据库;

2.用户首选项

2.1 场景介绍

用户首选项为应用提供Key-Value键值型的数据处理能力,支持应用持久化轻量级数据,并对其修改和查询。其会将数据缓存放在内存中,当用户读取的时候,能够从内存中快速获取数据,当需要持久化的时候可以使用flush接口将内存中的数据写入持久化文件中。

为了提升数据读写效率,用户首选项会将所有数据存到内存,随着数据量越多,会导致应用占用的内存越大,因此不适合存放过多的数据,适用的场景一般为应用保存用户的个性化设置等。

2.2 运作机制

image.png

从下往上看,用户首选项也是以文件的形式存储到应用沙箱内的,开发者可以将用户首选项持久化文件的内容加载到Preferences实例(一个文件对应一个Preferences实例),系统会通过静态容器将该实例存储在内存中,直到主动从内存中移除该实例或者删除该文件。

对于Key和Value有一些限制:

  • Key为string类型,非空且长度<=80个字节;
  • Value为string时,长度<=8192个字节;
  • 建议存储的数据不超过1万条;

2.3 接口说明

用户首选项和iOS的UserDefault,Android的SharedPreference的使用方法很像,都有基本的增、删、改、查方法,但不同的是用户首选项提供了订阅数据变更的能力:

// 获取Preferences实例。该接口存在异步接口。
getPreferencesSync(context: Context, options: Options): Preferences

// 将数据写入Preferences实例,可通过flush将Preferences实例持久化。该接口存在异步接口。
putSync(key: string, value: ValueType): void

// 检查Preferences实例是否包含名为给定Key的存储键值对。给定的Key值不能为空。该接口存在异步接口。
hasSync(key: string): void

// 获取键对应的值,如果值为null或者非默认值类型,返回默认数据defValue。该接口存在异步接口。
getSync(key: string, defValue: ValueType): void

// 从Preferences实例中删除名为给定Key的存储键值对。该接口存在异步接口。
deleteSync(key: string): void

// 将当前Preferences实例的数据异步存储到用户首选项持久化文件中。
flush(callback: AsyncCallback<void>): void

// 订阅数据变更,订阅的数据发生变更后,在执行flush方法后,触发callback回调。
on(type: 'change', callback: Callback<string>): void

// 取消订阅数据变更。
off(type: 'change', callback?: Callback<string>): void

// 从内存中移除指定的Preferences实例。若Preferences实例有对应的持久化文件,则同时删除其持久化文件。
deletePreferences(context: Context, options: Options, callback: AsyncCallback<void>): void

2.4 举例说明

// 导入包
import dataPreferences from '@ohos.data.preferences';

// 获取context
let context = getContext(this);
export default class PreferencesManager {

  static shared = new PreferencesManager();
  preferences?: dataPreferences.Preferences;
  preferencesName: string = 'CommonPreferences';
  
  // 初始化preferences实例
  initPreferences() {
    this.preferences = dataPreferences.getPreferencesSync(context, { name: this.preferencesName });
  }
  
  // 设置数据
  set(key: string, value: dataPreferences.ValueType) {
    if (!this.preferences) {
      this.initPreferences();
    }
    this.preferences?.putSync(key, value);
    this.preferences?.flush();
  }

  // 获取数据
  get(key: string): dataPreferences.ValueType | null | undefined {
    if (!this.preferences) {
      this.initPreferences();
    }
    let value = this.preferences?.getSync(key, null);;
    return value;
  }

  // 删除数据
  delete(key: string) {
    if (!this.preferences) {
      this.initPreferences();
    }
    if (this.preferences?.hasSync(key)) {
      this.preferences.deleteSync(key);
      this.preferences.flush();
    }
  }
}

3. 键值型数据库

3.1 介绍

键值型数据库用于存储键值对形式的数据,适用于简单的存储场景:比如存储商品名称及对应价格、员工工号及今日是否已出勤等。

  • 针对于单版本数据库,针对每条记录,Key的长度<=1KB,Value的长度<4MB;
  • 设备协同数据库,针对每条记录,Key的长度<=896 Byte, Value的长度<4MB;
  • 每个应用程序最多支持同时打开16个键值型分布式数据库;
  • 键值型数据库事件回调方法中不允许进行阻塞操作,例如修改UI组件;

3.2 接口说明

键值型数据库相关的接口,大部分为异步接口,均有callback和Promise两种返回形式。

// 创建一个KVManager对象实例,用于管理数据库对象
createKVManager(config: KVManagerConfig): KVManager

// 指定Options和storeId,创建并得到指定类型的KVStore数据库
getKVStore<T>(storeId: string, options: Options, callback: AsyncCallback<T>): void

// 添加指定类型的键值对到数据库
put(key: string, value: Uint8Array|string|number|boolean, callback: AsyncCallback<void>): void

// 获取指定键的值
get(key: string, callback: AsyncCallback<Uint8Array|string|boolean|number>): void

// 从数据库中删除指定键值的数据
delete(key: string, callback: AsyncCallback<void>): void

3.3 举例说明

// 导入模块
import distributedKVStore from '@ohos.data.distributedKVStore';

// 获取context
let context = getContext(this);
export default class KVStoreManager {
  
  // 单例模式
  static shared = new KVStoreManager();
  
  // SingleKVStore实例
  KVStore?: distributedKVStore.SingleKVStore;

  // 创建并获取KVStore
  async initKVStore() {
    try {
      // Create KVManager
      let bundleName = await getBundleName();
      const kvManagerConfig: distributedKVStore.KVManagerConfig = {
        context: context,
        bundleName: bundleName
      };
      const manager = distributedKVStore.createKVManager(kvManagerConfig);

      // Create KVStore
      const options: distributedKVStore.Options = {
        createIfMissing: true,
        encrypt: false,
        backup: false,
        autoSync: false,
        kvStoreType: distributedKVStore.KVStoreType.SINGLE_VERSION,
        securityLevel: distributedKVStore.SecurityLevel.S1
      };
      this.KVStore = await manager.getKVStore<distributedKVStore.SingleKVStore>('storeId', options);
    } catch (error) {
      console.log('[KVStoreManager]', `Failed to initKVStore, Cause: ${error}`);
    }
  }
  
  // 插入数据
  async set(key: string, value: Uint8Array | string | number | boolean) {
    if (!this.KVStore) {
      await this.initKVStore();
    }
    try {
      await this.KVStore?.put(key, value);
    } catch (error) {
      console.log('[KVStoreManager]', `Failed to set value, Cause: ${error}`);
    }
  }
 
  // 获取数据
  async get(key: string): Promise<string | number | boolean | Uint8Array | null | undefined> {
    if (!this.KVStore) {
      await this.initKVStore();
    }
    let value: Uint8Array | string | number | boolean | null | undefined = null;
    try {
      return this.KVStore?.get(key);
    } catch (error) {
      console.log('[KVStoreManager]', `Failed to get value, Cause: ${error}`);
    }
    return value;
  }
  
  // 删除数据
  async delete(key: string) {
    if (!this.KVStore) {
      await this.initKVStore();
    }
    try {
      await this.KVStore?.delete(key);
    } catch (error) {
      console.log('[KVStoreManager]', `Failed to delete value, Cause: ${error}`);
    }
  }
}

4. 关系型数据库

4.1 介绍

关系型数据库基于SQLite组件,适用于包含复杂关系数据的场景。比如购物车的商品数据,需要包括商品名称、价格、库存等。

基本概念:

  • 谓词:用来定义数据库的操作条件;
  • 结果集:执行查询之后的结果集合;

运作机制: 关系型数据库对应用提供通用的操作接口,底层使用SQLite作为持久化存储引擎,支持SQLite具有的数据库特性。

image.png

  • 数据库同一时间只能支持一个写操作;
  • ArkTS侧支持的基本数据类型:number、string、二进制类型数据、boolean;
  • 为保证插入并读取数据成功,一条数据不要超过2M;

4.2 接口说明

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

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

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

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

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

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

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

4.3 举例说明

// 导入模块
import relationalStore from '@ohos.data.relationalStore';

// 获取context
let context = getContext(this);
export default class RDBStoreManager {

  private rdbStore: relationalStore.RdbStore | null = null;
  private tableName: string;
  private sqlCreateTable: string;
  private databaseName: string = '__Database.db';

  constructor(tableName: string, sqlCreateTable: string) {
    this.tableName = tableName;
    this.sqlCreateTable = sqlCreateTable;
  }
  
  // 创建rdbStore
  async getRdbStore() {
    if (!this.rdbStore) {
      console.log('RDBStoreManager', 'The rdbStore exists.');
      return
    }
    try {
      let rdb = await relationalStore.getRdbStore(context, {
        name: this.databaseName, // 数据库文件名
        securityLevel: relationalStore.SecurityLevel.S1, // 数据库安全级别
        encrypt: false // 可选参数,是否加密,默认不加密
      });
      this.rdbStore = rdb;
      this.rdbStore.executeSql(this.sqlCreateTable);
    } catch (error) {
      console.log('RDBStoreManager', `gerRdbStore() failed, err: ${error}`);
    }
  }
  
  // 插入数据
  async insertData(data: relationalStore.ValuesBucket): Promise<number | null> {
    const valueBucket: relationalStore.ValuesBucket = data;
    if (this.rdbStore) {
      try {
        let result = await this.rdbStore.insert(this.tableName, valueBucket);
        return result;
      } catch (error) {
        console.log('RDBStoreManager', `insertData() failed, err: ${JSON.stringify(error)}`);
      }
    }
    return null;
  }
  
  // 删除数据
  async deleteData(predicates: relationalStore.RdbPredicates): Promise<number | null> {
    if (this.rdbStore) {
      try {
        let result = await this.rdbStore.delete(predicates);
        return result;
      } catch (error) {
        console.log('RDBStoreManager', `deleteData() failed, err: ${error}`);
      }
    }
    return null;
  }
  
  // 更新数据
  async updateData(predicates: relationalStore.RdbPredicates, data: relationalStore.ValuesBucket): Promise<number | null> {
    if (this.rdbStore) {
      try {
        let result = await this.rdbStore.update(data, predicates);
        return result;
      } catch (error) {
        console.log('RDBStoreManager', `updateData() failed, err: ${error}`);
      }
    }
    return null;
  }
  
  // 查询数据
  async query(predicates: relationalStore.RdbPredicates): Promise<relationalStore.ResultSet | null> {
    if (this.rdbStore) {
      try {
        let result = await this.rdbStore.query(predicates)
        return result;
      } catch (error) {
        console.log('RDBStoreManager', `query() failed, err: ${error}`);
      }
    }
    return null;
  }
}

在以上3个举例说明中,是自己根据业务需求封装的工具类,在实际使用过程中还需要进行一些更改,仅作参考。