简介

数据的本地存储在应用开发过程中重要的组成部分,本文将重点讨论Open Harmony和Android应用开发在数据本地存储方面存在的差异,分析它们各自的特点和优势。了解这些差异将有助于开发者更好地适应和利用不同的开发环境。

系统版本

OpenHarmony:4.0 Beta2

Android版本:13

键值对存储

OpenHarmony

在OpenHarmony应用开发中使用用户首选项键值型数据库两种方式来存储键值对形式的数据:

用户首选项(Preferences)

通常用于保存应用的配置信息。数据通过文本的形式保存在设备中,应用使用过程中会将文本中的数据全量加载到内存中,所以访问速度快、效率高,但不适合需要存储大量数据的场景。以下是用户首选项持久化功能的相关接口,更多接口及使用方式请见用户首选项

接口名称描述
getPreferences(context: Context, name: string, callback: AsyncCallback<Preferences>): void获取Preferences实例。
putSync(key: string, value: ValueType): void将数据写入Preferences实例,可通过flush将Preferences实例持久化。该接口存在异步接口。
hasSync(key: string): void检查Preferences实例是否包含名为给定Key的存储键值对。给定的Key值不能为空。该接口存在异步接口。
getSync(key: string, defValue: ValueType): void获取键对应的值,如果值为null或者非默认值类型,返回默认数据defValue。该接口存在异步接口。
deleteSync(key: string): void从Preferences实例中删除名为给定Key的存储键值对。该接口存在异步接口。
flush(callback: AsyncCallback<void>): void将当前Preferences实例的数据异步存储到用户首选项持久化文件中。
on(type: 'change', callback: Callback<{ key : string }>): void订阅数据变更,订阅的Key的值发生变更后,在执行flush方法后,触发callback回调。
off(type: 'change', callback?: Callback<{ key : string }>): void取消订阅数据变更。
deletePreferences(context: Context, name: string, callback: AsyncCallback<void>): void从内存中移除指定的Preferences实例。若Preferences实例有对应的持久化文件,则同时删除其持久化文件。

键值型数据库(KV-Store)

一种非关系型数据库,其数据以“键值”对的形式进行组织、索引和存储,其中“键”作为唯一标识符。适合很少数据关系和业务关系的业务数据存储,同时因其在分布式场景中降低了解决数据库版本兼容问题的复杂度,和数据同步过程中冲突解决的复杂度而被广泛使用。相比于关系型数据库,更容易做到跨设备跨版本兼容。以下是键值型数据库持久化功能的相关接口,大部分为异步接口。异步接口均有callback和Promise两种返回形式,下表均以callback形式为例,更多接口及使用方式请见分布式键值数据库

接口名称描述
createKVManager(config: KVManagerConfig): KVManager创建一个KVManager对象实例,用于管理数据库对象。
getKVStore<T>(storeId: string, options: Options, callback: AsyncCallback<T>): void指定Options和storeId,创建并得到指定类型的KVStore数据库。
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从数据库中删除指定键值的数据。

Android

在Android应用开发中推荐使用Jetpack DataStore来存储键值对数据,它可以存储键值对或类型化对象。DataStore 使用 Kotlin 协程和 Flow 以异步、一致的事务方式存储数据。DataStore 提供两种不同的实现:Preferences DataStore 和 Proto DataStore。

Preferences DataStore

  • 创建 Preferences DataStore

    RxJava使用 RxPreferenceDataStoreBuilder。必需的name参数是 Preferences DataStore 的名称。

val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "settings")
  • 从 Preferences DataStore 读取内容

    由于 Preferences DataStore 不使用预定义的架构,因此必须使用相应的键类型函数为需要存储在 DataStore<Preferences> 实例中的每个值定义一个键。例如,如需为 int 值定义一个键,请使用 intPreferencesKey()。然后,使用 DataStore.data 属性,通过 Flow 提供适当的存储值。

val EXAMPLE_COUNTER = intPreferencesKey("example_counter")
val exampleCounterFlow: Flow<Int> = context.dataStore.data
  .map { preferences ->
    // No type safety.
    preferences[EXAMPLE_COUNTER] ?: 0
}
  • 向 Preferences DataStore 写入数据

    Preferences DataStore 提供了一个 edit() 函数,用于以事务方式更新 DataStore 中的数据。该函数的 transform 参数接受代码块,您可以在其中根据需要更新值。转换块中的所有代码均被视为单个事务。

suspend fun incrementCounter() {
  context.dataStore.edit { settings ->
    val currentCounterValue = settings[EXAMPLE_COUNTER] ?: 0
    settings[EXAMPLE_COUNTER] = currentCounterValue + 1
  }
}

 

Proto DataStore 

  • 创建 Proto DataStore 来存储类型化对象涉及两个步骤:

    1. 定义一个实现Serializer<T>的类,其中T是 proto 文件中定义的类型。此序列化器类会告知 DataStore 如何读取和写入您的数据类型。请务必为该序列化器添加默认值,以便在尚未创建任何文件时使用。

    2. 使用dataStore所创建的属性委托来创建DataStore<T>实例,其中T是在 proto 文件中定义的类型。在您的 Kotlin 文件顶层调用该实例一次,便可在应用的所有其余部分通过此属性委托访问该实例。filename参数会告知 DataStore 使用哪个文件存储数据,而serializer参数会告知 DataStore 在第 1 步中定义的序列化器类的名称。
object SettingsSerializer : Serializer<Settings> {
  override val defaultValue: Settings = Settings.getDefaultInstance()
​
  override suspend fun readFrom(input: InputStream): Settings {
    try {
      return Settings.parseFrom(input)
    } catch (exception: InvalidProtocolBufferException) {
      throw CorruptionException("Cannot read proto.", exception)
    }
  }
​
  override suspend fun writeTo(
    t: Settings,
    output: OutputStream) = t.writeTo(output)
}
​
val Context.settingsDataStore: DataStore<Settings> by dataStore(
  fileName = "settings.pb",
  serializer = SettingsSerializer
)
  • 从 Proto DataStore 读取数据

val exampleCounterFlow: Flow<Int> = context.settingsDataStore.data
  .map { settings ->
    // The exampleCounter property is generated from the proto schema.
    settings.exampleCounter
  }
  • 向 Proto DataStore 写入数据

总结

在存储方式上,两者都能将数据进行落盘操作,保证在重启设备后数据还存在,但OpenHarmony中的用户首选项方式需要手动落盘,这样可以存储一些临时数据,避免大量数据落盘而未及时清除时导致磁盘占用过大,OpenHarmony中使用用户首选项时还可以监听数据的变化,方便相关业务及时相应数据更新。

数据库存储

OpenHarmony

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

OpenHarmony应用开发中关系型数据库对应用提供通用的操作接口,支持SQLite具有的数据库特性,包括但不限于事务、索引、视图、触发器、外键、参数化查询和预编译SQL语句。

以下是关系型数据库持久化功能的相关接口,大部分为异步接口。异步接口均有callback和Promise两种返回形式,下表均以callback形式为例,更多接口及使用方式请见关系型数据库

接口名称描述
getRdbStore(context: Context, config: StoreConfig, callback: AsyncCallback<RdbStore>): void获得一个相关的RdbStore,操作关系型数据库,用户可以根据自己的需求配置RdbStore的参数,然后通过RdbStore调用相关接口可以执行相关的数据操作。
executeSql(sql: string, bindArgs: Array<ValueType>, callback: AsyncCallback<void>):void执行包含指定参数但不返回值的SQL语句。
insert(table: string, values: ValuesBucket, callback: AsyncCallback<number>):void向目标表中插入一行数据。
update(values: ValuesBucket, predicates: RdbPredicates, callback: AsyncCallback<number>):void根据RdbPredicates的指定实例对象更新数据库中的数据。
delete(predicates: RdbPredicates, callback: AsyncCallback<number>):void根据RdbPredicates的指定实例对象从数据库中删除数据。
query(predicates: RdbPredicates, columns: Array<string>, callback: AsyncCallback<ResultSet>):void根据指定条件查询数据库中的数据。
deleteRdbStore(context: Context, name: string, callback: AsyncCallback<void>): void删除数据库。

Android

Android在应用开发中使用 Room,Room 持久性库在 SQLite 上提供了一个抽象层,以便在充分利用 SQLite 的强大功能的同时,能够流畅地访问数据库。

Room 包含三个主要组件:

  1. 数据库类,用于保存数据库并作为应用持久性数据底层连接的主要访问点。

  2. 数据实体,用于表示应用的数据库中的表。

  3. 数据访问对象 (DAO),提供您的应用可用于查询、更新、插入和删除数据库中的数据的方法。 数据库类为应用提供与该数据库关联的 DAO 的实例。反过来,应用可以使用 DAO 从数据库中检索数据,作为关联的数据实体对象的实例。此外,应用还可以使用定义的数据实体更新相应表中的行,或者创建新行供插入。

单个数据实体和单个 DAO 的 Room 数据库实现示例如下:

以下代码定义了一个User数据实体。User的每个实例都代表应用数据库中User表中的一行。

@Entity
data class User(
    @PrimaryKey val uid: Int,
    @ColumnInfo(name = "first_name") val firstName: String?,
    @ColumnInfo(name = "last_name") val lastName: String?
)

定义一个名为UserDao的 DAO。UserDao提供了应用的其余部分用于与user表中的数据交互的方法。

@Dao
interface UserDao {
    @Query("SELECT * FROM user")
    fun getAll(): List<User>
​
    @Query("SELECT * FROM user WHERE uid IN (:userIds)")
    fun loadAllByIds(userIds: IntArray): List<User>
​
    @Query("SELECT * FROM user WHERE first_name LIKE :first AND " +
           "last_name LIKE :last LIMIT 1")
    fun findByName(first: String, last: String): User
​
    @Insert
    fun insertAll(vararg users: User)
​
    @Delete
    fun delete(user: User)
}

定义用于保存数据库的AppDatabase类。AppDatabase定义数据库配置,并作为应用对持久性数据的主要访问点。数据库类必须满足以下条件:

  • 该类必须带有 @Database 注解,该注解包含列出所有与数据库关联的数据实体的 entities 数组。

  • 该类必须是一个抽象类,用于扩展 RoomDatabase。

  • 对于与数据库关联的每个 DAO 类,数据库类必须定义一个具有零参数的抽象方法,并返回 DAO 类的实例。

@Database(entities = [User::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
    abstract fun userDao(): UserDao
}

定义数据实体、DAO 和数据库对象后,创建数据库实例:

val db = Room.databaseBuilder(
            applicationContext,
            AppDatabase::class.java, "database-name"
        ).build()

使用 Room DAO 访问数据,每个 DAO 定义为一个接口或一个抽象类。对于基本用例,您通常应使用接口。无论是哪种情况,您都必须始终使用 @Dao 为您的 DAO 添加注解。DAO 不具有属性,但它们定义了一个或多个方法,可用于与应用数据库中的数据进行交互。

例如查询所有数据:

val userDao = db.userDao()
val users: List<User> = userDao.getAll()

 

总结

OpenHarmony与Android均选择使用SQLite进行关系性数据存储,在OpenHarmony中直接封装了数据库相关增删改查的napi接口,也提供了可以直接运行sql语句的接口,但是在Android中,则在SQLite上提供了一个抽象层,需要开发者手动实现相关的数据实体和数据的增删改查接口。

Logo

社区规范:仅讨论OpenHarmony相关问题。

更多推荐