基于鸿蒙OS开发静脉输液智能监控系统(5)-数据持久化服务设计
基于鸿蒙OS开发静脉输液智能监控系统(5)-数据持久化服务设计
1. HarmonyOS 数据存储方案对比
HarmonyOS 为应用开发者提供了多种数据持久化方案,每种方案都有其特定的适用场景和性能特征。在 IVGuard 项目的架构设计阶段,我们需要对这些方案进行深入对比分析,以选择最适合 MVP 阶段的数据持久化技术路线。
1.1 Preferences(轻量键值对存储)
Preferences 是 HarmonyOS 提供的轻量级键值对(Key-Value)存储方案,其底层实现基于 XML 文件格式。它适用于存储少量、轻量的结构化数据,如应用配置信息、用户偏好设置、简单的状态标记等。
核心特征:
- 存储格式:XML 文件,以键值对形式组织数据
- 数据量限制:建议存储数据不超过 1KB ~ 10KB 级别,键值对条目建议不超过数百条
- 读写模式:提供同步读写 API(
putSync、getSync)和异步持久化 API(flush) - ValueType 支持:仅支持
string、number、boolean、Array<string>、Array<number>、Array<boolean>等基础类型,不支持直接存储对象(Object) - 使用场景:应用配置、开关状态、简单标记、小型数据集合
- 线程模型:单进程内共享实例,不支持多进程并发写入
优势:
- API 简洁直观,学习成本极低
- 同步读写保证了代码的线性可读性
- 异步
flush()机制允许开发者精确控制磁盘写入时机 - 初始化开销小,无需创建数据库表结构
- 适合移动端应用的小数据量快速存取场景
劣势:
- 不支持复杂查询(无 SQL 能力)
- 不适合存储大量结构化数据
- 不支持跨设备同步
- 值类型有限,存储复杂对象需手动序列化/反序列化
1.2 关系型数据库(RDB)
关系型数据库基于 SQLite 引擎,提供了完整的 SQL 查询能力,适用于需要复杂查询、事务支持、多表关联的结构化数据持久化场景。
核心特征:
- 存储格式:SQLite 数据库文件
- 数据量限制:可支持 GB 级别的数据存储
- 查询能力:完整的 SQL-92 标准支持,包括 JOIN、子查询、聚合函数等
- 事务支持:ACID 事务保证,支持
BEGIN/COMMIT/ROLLBACK - ValueType 支持:NULL、INTEGER、REAL、TEXT、BLOB 五种基本存储类
- 使用场景:结构化业务数据、多表关联查询、数据分析报表
优势:
- 强大的查询能力,可应对复杂业务场景
- 事务保证数据一致性和完整性
- 数据量扩展性好,性能随数据量增长的衰减可控
- 支持索引优化查询性能
- 成熟的 SQLite 生态,社区资源丰富
劣势:
- 需要预先定义表结构(Schema),增加了开发复杂度
- 数据库版本迁移需要额外的升级脚本管理
- 初始化开销较大,需要创建数据库和表结构
- SQL 语句编写和调试成本高于简单的 KV 操作
- 对于极少量数据,性能优势无法体现
1.3 分布式数据服务(DistributedData)
分布式数据服务是 HarmonyOS 面向 IoT 和多设备协同场景提供的数据同步方案,支持跨设备的数据自动同步和冲突解决。
核心特征:
- 存储格式:基于分布式键值对存储
- 同步能力:自动跨设备数据同步,支持同账号下的多设备数据一致性
- 冲突解决:内置 Last-Write-Win 等冲突解决策略
- 使用场景:多设备协同、IoT 设备数据同步、跨端状态共享
优势:
- 开箱即用的跨设备数据同步
- 自动冲突解决,降低开发复杂度
- 适合分布式场景
劣势:
- 依赖 HarmonyOS 分布式能力,设备需在同一网络环境下
- 数据同步延迟不可控,不适合实时性要求高的场景
- 调试和测试复杂度高,需要多设备环境
- MVP 阶段不必要的基础设施复杂度
1.4 文件存储
文件存储是最基础的持久化方案,适用于大文件、媒体资源的直接存储,也支持自定义格式的小数据量存储。
核心特征:
- 存储格式:任意格式,由开发者自定义
- 数据量限制:仅受设备存储空间限制
- 访问模式:基于文件路径的读写操作
- 使用场景:图片、视频、文档、日志文件、数据导出
优势:
- 完全灵活,不受数据格式限制
- 适合存储大型二进制数据
- 可与外部系统(PC、云端)通过文件交换数据
劣势:
- 无查询能力,需要全量加载后手动筛选
- 并发控制需要自行实现
- 数据一致性需要自行保证
- 不适合频繁更新的结构化数据
1.5 IVGuard MVP 阶段的选择分析
在 IVGuard 项目的 MVP 阶段,我们需要存储以下几类数据:
| 数据类型 | 预估数据量 | 读写频率 | 查询复杂度 |
|---|---|---|---|
| Medicine(药物列表) | < 20 条 | 低频写入,中频读取 | 按 ID 查找 |
| MonitorSession(监控会话) | < 10 条 | 中频写入,中频读取 | 按 ID 查找 |
| MonitorRecord(完成记录) | < 50 条 | 低频写入,中频读取 | 按时间排序 |
| AlertEvent(预警事件) | < 30 条 | 中频写入,高频读取 | 按 handled 过滤 |
| CostItem(费用项) | < 50 条 | 中频写入,中频读取 | 按分类聚合 |
| InsurancePolicy(医保政策) | 1 条 | 极低频写入,低频读取 | 单条读取 |
| AppSettings(应用配置) | 1 条 | 极低频写入,高频读取 | 单条读取 |
| LevelRecord(液位时序) | < 100 条 | 高频写入,中频读取 | 按 sessionId 过滤 |
数据量分析: 所有实体合计不超过 300 条记录,总数据量估算在 50KB ~ 200KB 范围内,远低于 Preferences 的推荐上限。
查询需求分析: MVP 阶段的查询均为简单的按 ID 查找、全量加载后内存筛选,不需要 SQL 级别的复杂查询能力。
基于以上分析,选择 Preferences 作为 IVGuard MVP 阶段的数据持久化方案:
- 数据量匹配:总数据量远低于 Preferences 推荐上限
- 查询需求匹配:无需复杂查询,全量加载后内存筛选即可满足
- 开发效率:API 简洁,无需定义表结构,可快速迭代
- 性能足够:同步读写保证了读取的即时性,异步 flush 保证了写入的可靠性
- 迁移路径清晰:DataStore 封装了存储细节,未来可无缝切换到 RDB
2. Preferences API 详解
2.1 模块导入与初始化
Preferences API 位于 @kit.ArkData 模块中,使用前需要进行模块导入:
import { preferences } from '@kit.ArkData'
import { common } from '@kit.AbilityKit'
其中 @kit.ArkData 提供数据持久化的核心 API,@kit.AbilityKit 提供 UIAbilityContext 上下文,这是初始化 Preferences 实例的必要参数。
初始化方法:
const store: preferences.Preferences = preferences.getPreferencesSync(
context,
{ name: 'ivguard_data' }
)
getPreferencesSync 方法接收两个参数:
- context:
common.UIAbilityContext类型,提供应用沙箱路径,Preferences 文件将存储在该路径下 - options:包含
name字段的配置对象,name即为 Preferences 文件的名称,底层将创建名为ivguard_data.xml的文件
该方法为同步调用,立即返回 preferences.Preferences 实例。多次调用同一 name 将返回同一实例(单例语义),不会创建多个文件副本。
2.2 读写操作 API
Preferences 提供了同步读写和异步持久化两套 API,二者的分工如下:
写入操作 — putSync(key: string, value: ValueType)
store.putSync('medicines', JSON.stringify(medicines))
putSync 将键值对写入内存中的 Preferences 缓存。此操作是同步的,调用返回后即可通过 getSync 读取到最新值。但此时数据尚未持久化到磁盘文件,如果应用异常退出,这部分数据将丢失。
读取操作 — getSync(key: string, defaultValue: ValueType)
const raw = store.getSync('medicines', '[]') as string
getSync 从内存缓存中读取指定键的值。如果键不存在,则返回 defaultValue。此操作是同步的,保证了读取的即时性。
持久化操作 — flush()
await store.flush()
flush 将内存中的数据异步写入磁盘文件。这是一个异步操作,返回 Promise<void>。调用后数据将真正持久化到 ivguard_data.xml 文件中,即使应用异常退出,数据也不会丢失。
2.3 ValueType 类型窄化
Preferences 的 ValueType 定义如下:
type ValueType = string | number | boolean | Array<string> | Array<number> | Array<boolean>
这意味着 Preferences 不支持直接存储对象(Object)。对于复杂的数据结构,必须先进行 JSON 序列化,将其转换为 string 类型后再存储。
关键限制:
- 不支持直接存储自定义类实例
- 不支持嵌套对象
- 不支持
Array<Object>类型 - 不支持
undefined或null值 Array仅支持基本类型数组,不支持对象数组
解决方案:
写入时使用 JSON.stringify() 将对象/数组转为字符串:
s.putSync('medicines', JSON.stringify(medicines))
读取时使用 JSON.parse() 将字符串转回原始结构,然后手动反序列化为类实例:
const raw = s.getSync('medicines', '[]') as string
const arr: Object[] = JSON.parse(raw) as Object[]
// 遍历 arr,手动创建类实例并逐字段赋值
2.4 为什么不能直接存储对象
在标准 TypeScript/JavaScript 环境中,JSON.stringify 和 JSON.parse 可以方便地处理对象的序列化与反序列化。但在 ArkTS 严格模式下,存在以下限制:
JSON.parse返回类型为Object:ArkTS 不允许直接将Object类型赋值给自定义类实例,因为类型信息在序列化过程中已丢失- 禁止
Object.assign:ArkTS 严格模式禁止使用Object.assign等反射式赋值操作,因为其类型不安全 - 禁止
as直接强转Object到自定义类:虽然语法上可以通过as进行类型断言,但运行时对象仍然只是普通Object,不包含类的方法和原型链信息 - 类实例需要通过
new创建:ArkTS 要求通过构造函数new ClassName()创建类实例,然后逐字段赋值
因此,IVGuard 的 DataStore 采用了手动反序列化策略:JSON.parse 后得到 Object[],遍历每个元素转为 Record<string, Object>,然后通过逐字段类型断言和赋值,创建新的类实例。
3. DataStore 架构设计
3.1 单例模式
DataStore 采用静态类 + 静态属性的方式实现单例模式,确保全局只有一个 Preferences 实例:
export class DataStore {
private static store: preferences.Preferences | undefined = undefined
// ...
}
设计考量:
- 全局唯一:
private static store保证了整个应用生命周期内只有一个 Preferences 实例 - 延迟初始化:
undefined初始值允许在应用启动时按需初始化,而非在类加载时立即创建 - 私有访问:
private修饰符阻止外部直接操作 Preferences 实例,所有操作必须通过 DataStore 的公共方法 - 未初始化保护:
| undefined联合类型使得编译器和运行时都能检测到未初始化的状态
3.2 异步初始化流程
DataStore 的初始化通过 init 静态方法完成:
static async init(context: common.UIAbilityContext): Promise<void> {
if (DataStore.store !== undefined) {
return
}
DataStore.store = preferences.getPreferencesSync(context, { name: 'ivguard_data' })
}
流程解析:
- 幂等性保护:
if (DataStore.store !== undefined) return确保多次调用init不会重复初始化。这在多页面场景下至关重要,因为每个页面的aboutToAppear都会调用DataStore.init() - 同步创建实例:
getPreferencesSync是同步调用,立即返回 Preferences 实例 - 异步方法签名:尽管内部是同步操作,
init方法的返回类型是Promise<void>,这为未来可能的异步初始化需求(如数据迁移、版本检查)预留了扩展空间 - Context 依赖:需要
UIAbilityContext参数来获取应用沙箱路径,这是创建 Preferences 文件的前提条件
为什么用 async 包裹同步操作?
这是有意的架构设计。虽然 getPreferencesSync 本身是同步的,但将 init 设计为 async 方法有以下好处:
- 调用方使用
.then()语法,确保初始化完成后再执行数据加载 - 未来如果初始化逻辑变为异步(如从旧版本迁移数据),调用方无需修改
- 统一的异步接口风格,与
save*方法保持一致
3.3 getStore() 安全检查
private static getStore(): preferences.Preferences {
if (DataStore.store === undefined) {
throw new Error('DataStore not initialized')
}
return DataStore.store
}
getStore() 是所有公共方法的内部辅助方法,它执行以下安全检查:
- 未初始化检测:如果
store仍为undefined,说明init()尚未被调用或尚未完成 - 快速失败:抛出
Error而非静默返回undefined,避免后续操作产生难以追踪的空引用异常 - 类型收窄:通过类型守卫(
undefined检查),TypeScript 编译器能够将store的类型从preferences.Preferences | undefined收窄为preferences.Preferences,消除了后续操作的可空性警告
每个 save/load 方法的第一行都是 const s = DataStore.getStore(),这种模式确保了:
- 统一的初始化检查入口
- 减少重复的
undefined判断代码 - 方法体内部可以安全地使用
s而无需担心空引用
3.4 实体类型的 save/load 方法对
DataStore 为每种持久化实体提供了一对 save/load 方法:
| 方法对 | 实体类型 | 存储键 | 值类型 |
|---|---|---|---|
saveMedicines / loadMedicines |
Medicine[] |
'medicines' |
JSON 字符串(数组) |
saveSessions / loadSessions |
MonitorSession[] |
'sessions' |
JSON 字符串(数组) |
saveRecords / loadRecords |
MonitorRecord[] |
'records' |
JSON 字符串(数组) |
saveAlerts / loadAlerts |
AlertEvent[] |
'alerts' |
JSON 字符串(数组) |
saveCostItems / loadCostItems |
CostItem[] |
'costItems' |
JSON 字符串(数组) |
saveInsurancePolicy / loadInsurancePolicy |
InsurancePolicy |
'insurancePolicy' |
JSON 字符串(单条) |
saveSettings / loadSettings |
AppSettings |
'settings' |
JSON 字符串(单条) |
saveLevelRecords / loadLevelRecords |
LevelRecord[] |
'levelRecords' |
JSON 字符串(数组) |
命名约定:
save方法:async save{EntityName}(data: Type): Promise<void>load方法:load{EntityName}(): Type(同步返回)
save 方法的统一模式:
static async saveXxx(data: Type): Promise<void> {
const s = DataStore.getStore() // 1. 安全获取 store 实例
s.putSync('key', JSON.stringify(data)) // 2. 同步写入内存缓存
await s.flush() // 3. 异步持久化到磁盘
}
load 方法的统一模式(数组类型):
static loadXxx(): Type[] {
const s = DataStore.getStore() // 1. 安全获取 store 实例
const raw = s.getSync('key', '[]') as string // 2. 读取 JSON 字符串
const arr: Object[] = JSON.parse(raw) as Object[] // 3. 解析为 Object 数组
const result: Type[] = []
for (let i = 0; i < arr.length; i++) {
const item = arr[i] as Record<string, Object> // 4. 转为 Record
const obj = new Type() // 5. 创建类实例
obj.field = (item['field'] as FieldType) ?? default // 6. 逐字段赋值
result.push(obj)
}
return result
}
load 方法的统一模式(单条记录类型):
static loadXxx(): Type {
const s = DataStore.getStore() // 1. 安全获取 store 实例
const raw = s.getSync('key', '') as string // 2. 读取 JSON 字符串
if (raw.length === 0) { // 3. 空值判断
return new Type() // 4. 返回默认实例
}
const item = JSON.parse(raw) as Record<string, Object> // 5. 解析为 Record
const obj = new Type() // 6. 创建类实例
obj.field = (item['field'] as FieldType) ?? default // 7. 逐字段赋值
return obj
}
4. JSON 序列化策略详解
4.1 完整序列化/反序列化流程
IVGuard 的 DataStore 采用了一套统一的 JSON 序列化策略,将类实例持久化到 Preferences 中。完整的流程如下:
写入流程(类实例 → Preferences):
类实例数组 → JSON.stringify() → JSON 字符串 → putSync(key, string) → 内存缓存
↓
flush() → 磁盘文件
读取流程(Preferences → 类实例):
磁盘文件 → 内存缓存 → getSync(key, default) → JSON 字符串 → JSON.parse() → Object[]
↓
Record<string, Object>
↓
逐字段类型断言 + 赋值 → 类实例数组
4.2 写入操作详解
写入操作的核心代码模式:
static async saveMedicines(medicines: Medicine[]): Promise<void> {
const s = DataStore.getStore()
s.putSync('medicines', JSON.stringify(medicines))
await s.flush()
}
步骤解析:
DataStore.getStore():获取已初始化的 Preferences 实例,未初始化则抛出错误JSON.stringify(medicines):将Medicine[]数组序列化为 JSON 字符串。由于Medicine类的所有字段都是基础类型(string、number),JSON.stringify能够完整地保留所有字段的值s.putSync('medicines', ...):将 JSON 字符串以'medicines'为键写入 Preferences 的内存缓存await s.flush():异步将内存缓存的数据持久化到磁盘文件ivguard_data.xml
JSON.stringify 的行为分析:
当对一个 Medicine 实例数组执行 JSON.stringify 时,输出类似:
[
{
"id": "1698765432100abc123",
"name": "头孢曲松钠",
"barcode": "6901234567890",
"dosage": "1g/支",
"volume": 500,
"stickerId": "",
"nfcTagId": "",
"drugDbId": "drug_001",
"addedAt": 1698765432100
}
]
注意,JSON.stringify 只序列化实例的可枚举自有属性,不会序列化类的原型方法(如 Medicine.create)。这意味着反序列化后得到的是纯数据对象,不包含类的行为方法,需要手动重建类实例。
4.3 读取操作详解(数组类型)
数组类型的读取是最复杂的部分,以 loadMedicines 为例:
步骤 1:读取原始 JSON 字符串
const raw = s.getSync('medicines', '[]') as string
- 使用
getSync从内存缓存中读取键为'medicines'的值 - 默认值为
'[]'(空数组的 JSON 表示),如果键不存在则返回此默认值 as string类型断言:因为我们总是存储字符串类型,所以可以安全地断言为string
步骤 2:解析 JSON 为 Object 数组
const arr: Object[] = JSON.parse(raw) as Object[]
JSON.parse(raw)将 JSON 字符串解析为 JavaScript 对象- 在 ArkTS 中,
JSON.parse的返回类型是Object,需要显式断言为Object[] - 此时
arr中的每个元素都是Object类型,无法直接访问其属性
步骤 3:遍历并逐元素反序列化
const result: Medicine[] = []
for (let i = 0; i < arr.length; i++) {
const item = arr[i] as Record<string, Object>
const m = new Medicine()
m.id = (item['id'] as string) ?? ''
m.name = (item['name'] as string) ?? ''
// ... 其他字段
result.push(m)
}
return result
每个元素的反序列化包含以下关键步骤:
- 转为
Record<string, Object>:arr[i] as Record<string, Object>将Object类型的元素转为可索引的 Record 类型,使得可以通过item['fieldName']访问属性 - 创建类实例:
new Medicine()通过构造函数创建一个所有字段为默认值的类实例 - 逐字段赋值:通过
item['field'] as Type访问并断言字段类型,然后赋值给类实例 - 空值保护:
?? defaultValue使用空值合并运算符,当字段不存在或为undefined/null时提供合理的默认值
4.4 读取操作详解(单条记录类型)
单条记录类型(如 InsurancePolicy、AppSettings)的读取模式略有不同:
static loadInsurancePolicy(): InsurancePolicy {
const s = DataStore.getStore()
const raw = s.getSync('insurancePolicy', '') as string
if (raw.length === 0) {
return new InsurancePolicy()
}
const item = JSON.parse(raw) as Record<string, Object>
const p = new InsurancePolicy()
p.insuranceType = (item['insuranceType'] as string) ?? ''
// ... 其他字段
return p
}
关键差异:
- 默认值为空字符串
'':而非'[]',因为单条记录不是数组 - 空值判断:
if (raw.length === 0)检查字符串长度是否为零,如果为空则返回一个所有字段为默认值的新实例 - 直接解析为
Record<string, Object>:无需数组遍历,因为只有一条记录 - 无需数组操作:没有
Object[]中间步骤和循环遍历
4.5 为什么不用 Object.assign
在标准 TypeScript 中,可以使用 Object.assign 将反序列化后的对象属性批量复制到类实例:
// 标准TypeScript中的写法(ArkTS中禁止)
const m = new Medicine()
Object.assign(m, arr[i])
但在 ArkTS 严格模式下,Object.assign 被禁止使用,原因如下:
- 类型不安全:
Object.assign是运行时反射操作,编译器无法在编译期检查属性名和类型的正确性。如果 JSON 中的属性名拼写错误或类型不匹配,编译器不会报错,但运行时会产生难以调试的问题 - ArkTS 类型系统限制:ArkTS 要求所有属性访问在编译时可确定,
Object.assign的动态特性违反了这一原则 - 原型链污染风险:
Object.assign可能意外地复制原型链上的属性,导致不可预期的行为 - 性能不可控:反射式属性复制的性能取决于属性数量和类型,无法在编译期优化
IVGuard 的替代方案:手动逐字段赋值,虽然代码冗长,但类型安全、行为确定、性能可控。
4.6 ?? 默认值处理策略
在反序列化过程中,每个字段都使用 ?? 运算符提供默认值:
m.id = (item['id'] as string) ?? ''
m.volume = (item['volume'] as number) ?? 0
m.isActive = (item['isActive'] as boolean) ?? false
?? vs || 的选择:
??(空值合并运算符):仅在值为null或undefined时使用默认值||(逻辑或运算符):在值为 falsy(0、''、false、null、undefined)时使用默认值
IVGuard 选择 ?? 而非 || 的原因:
- 数字字段:
0是合法的volume值,使用||会错误地将0替换为默认值 - 布尔字段:
false是合法的isActive值,使用||会错误地将false替换为true - 字符串字段:虽然
''使用||也会触发默认值,但??的语义更清晰,统一使用??可以保持代码一致性
默认值的设计原则:
string类型:默认值为''(空字符串),而非null或undefinednumber类型:默认值为0,个别字段如currentLevel默认为100(表示满液位)boolean类型:根据业务语义选择,如isActive默认false,voiceAlertEnabled默认true
5. 各实体持久化实现详解
5.1 Medicine 数组序列化
数据模型定义(DataModels.ets:1-21):
export class Medicine {
id: string = ''
name: string = ''
barcode: string = ''
dosage: string = ''
volume: number = 0
stickerId: string = ''
nfcTagId: string = ''
drugDbId: string = ''
addedAt: number = 0
}
Medicine 类包含 9 个字段,全部为基础类型(7 个 string + 2 个 number),无嵌套对象,适合直接 JSON 序列化。
saveMedicines 实现(DataStore.ets:22-26):
static async saveMedicines(medicines: Medicine[]): Promise<void> {
const s = DataStore.getStore()
s.putSync('medicines', JSON.stringify(medicines))
await s.flush()
}
写入逻辑简洁明了:将整个 Medicine[] 数组序列化为 JSON 字符串,以 'medicines' 为键存储。
loadMedicines 实现(DataStore.ets:28-48):
static loadMedicines(): Medicine[] {
const s = DataStore.getStore()
const raw = s.getSync(
更多推荐



所有评论(0)