HarmonyOS今天吃什么——数据库层的持久化设计与CRUD实现
2026/7/17 12:18:49 网站建设 项目流程

美食转盘的数据层藏了一个有意思的设计决策——用文件存储食物数据,用 Preferences 存主题设置。同样是持久化,为什么不能统一用一种方案?因为数据量不同:食物数组可能有几十上百条记录,每次转盘都要全量读取和筛选;主题设置只有一个字符串。文件存储适合"大块数据",Preferences 适合"小配置"。这个区分决定了整个数据层的架构。

完整效果

一、为什么用文件而不是 Preferences

两种持久化方案的对比

特性Preferences文件 (fileIo)
数据量单键 ≤ 1MB无限制
数据结构字符串/数字/布尔任意 JSON
读写方式key-value全量读/全量写
适合场景配置项(主题、开关)列表数据(食物、历史)

Preferences 是钥匙柜——每把钥匙对应一个小抽屉。文件是档案袋——一个袋子装一整份数据。食物数据需要按分类筛选、按收藏过滤、统计 pickCount——这些操作必须拿到完整数组才能做。用 Preferences 存 50 个食物需要 50 次 get 调用,用文件只需要 1 次 read。

这个 App 的数据分布

数据存储方案原因
食物列表foods.json 文件大数组,需要筛选
历史记录history.json 文件大数组,需要排序
主题设置Preferences单值,频繁读取

两种方案各管各的领地——不混用,不替代。

二、数据库类的初始化

两个文件路径

constructor(ctx:common.UIAbilityContext){this.fp=ctx.filesDir+'/foods.json';this.hp=ctx.filesDir+'/history.json';}

构造函数接收 UIAbilityContext——不是页面级的 Context。这是 Stage 模型的要求:fileIo 操作需要应用级 Context 的 filesDir 路径。页面级 Context 的 filesDir 可能不同。

init 方法的五步加载

asyncinit():Promise<void>{try{constfd=this.read(this.fp);this.foods=fd?JSON.parse(fd)asFoodItem[]:getDefaultFoods();if(!fd)this.saveFoods();consthd=this.read(this.hp);this.history=hd?JSON.parse(hd)asPickRecord[]:[];}catch(e){this.foods=getDefaultFoods();this.history=[];}}

五步操作:

步骤操作目的
1read(foods.json)读取食物数据
2判断文件是否存在有→解析,无→用默认数据
3首次写入默认数据给 foods.json 创建初始内容
4read(history.json)读取历史数据
5catch 兜底文件损坏时恢复默认

“有则解析,无则创建”——初始化逻辑兼顾首次运行和后续加载。catch 块防止 JSON.parse 报错导致整个数据库崩溃。

getDefaultFoods() 的作用

this.foods=fd?JSON.parse(fd)asFoodItem[]:getDefaultFoods();

首次运行时 foods.json 不存在,read 返回空字符串,fd 为 falsy——用 getDefaultFoods() 填充。然后立刻 saveFoods() 把默认数据写入文件——下次启动就能读到了。

三、文件读写的底层实现

read 方法

privateread(f:string):string{if(!fileIo.accessSync(f))return'';consts=fileIo.statSync(f);constfd=fileIo.openSync(f,fileIo.OpenMode.READ_ONLY);constab=newArrayBuffer(s.size);fileIo.readSync(fd.fd,ab);fileIo.closeSync(fd);returnbuffer.from(ab).toString();}

四步操作:

步骤API作用
1accessSync检查文件是否存在
2statSync获取文件大小
3openSync + readSync打开文件,读取全部内容到 ArrayBuffer
4buffer.from(ab).toString()把 ArrayBuffer 转成字符串

为什么用 ArrayBuffer 而不是直接读字符串?fileIo 的 readSync 接收 ArrayBuffer 参数——这是 HarmonyOS 文件 API 的设计。读完后用 buffer 模块转换成字符串。

write 方法

privatewrite(f:string,d:string):void{constb=buffer.from(d).buffer;constfd=fileIo.openSync(f,fileIo.OpenMode.CREATE|fileIo.OpenMode.WRITE_ONLY);fileIo.writeSync(fd.fd,b);fileIo.closeSync(fd);}

CREATE | WRITE_ONLY 模式——文件不存在就创建,存在就覆盖。每次写入都是全量替换——整个 foods.json 被新数据覆盖。这比增量写入简单,但数据量大时可能有性能问题。对于几十条食物记录来说,全量写入完全够用。

读写对称性

read: accessSync → statSync → openSync(READ_ONLY) → readSync → closeSync write: buffer.from → openSync(CREATE|WRITE_ONLY) → writeSync → closeSync

read 和 write 是对称的——一个读 ArrayBuffer 转字符串,一个字符串转 ArrayBuffer 写入。这是文件 I/O 的标准模式。

四、CRUD 操作的实现

Create——addFood

asyncaddFood(f:FoodItem):Promise<void>{this.foods.push(f);awaitthis.saveFoods();}

push 直接修改内存数组,然后异步写入文件。两步操作不是原子的——如果 push 后、saveFoods 前崩溃,内存数据和文件数据不一致。但对于本地 App 来说,这个窗口极小,可以接受。

Read——getFoods

getFoods(cat?:string):FoodItem[]{if(!cat||cat==='all')returnthis.foods.slice();returnthis.foods.filter((f:FoodItem)=>f.category===cat);}

slice() 返回浅拷贝——防止外部修改影响内部数据。filter() 返回新数组——不影响原数组。两个方法都保证了数据的不可变性。

Update——toggleFav

asynctoggleFav(id:string):Promise<void>{constf=this.foods.find((x:FoodItem)=>x.id===id);if(f){f.isFavorite=!f.isFavorite;awaitthis.saveFoods();}}

find 找到对象引用,直接修改属性。对象是引用类型——修改 f.isFavorite 等同于修改 this.foods[i].isFavorite。

Delete——deleteFood

asyncdeleteFood(id:string):Promise<void>{consti=this.foods.findIndex((f:FoodItem)=>f.id===id);if(i>=0){this.foods.splice(i,1);awaitthis.saveFoods();}}

findIndex 找到索引,splice 删除元素。splice 会修改原数组——这是预期行为。findIndex 比 find 多返回索引,用于 splice。

五、转盘抽签的 pick 方法

这是整个数据库最复杂的方法:

asyncpick(cat?:string):Promise<FoodItem|null>{constpool=this.getFoods(cat);if(pool.length===0)returnnull;constp=pool[Math.floor(Math.random()*pool.length)];p.pickCount++;p.lastPicked=Date.now();awaitthis.saveFoods();this.history.push({id:'h_'+Date.now(),foodName:p.name,emoji:p.emoji,category:p.category,pickedAt:Date.now()}asPickRecord);awaitthis.saveHistory();returnp;}

四步操作的顺序

步骤操作原因
1getFoods(cat)按分类筛选候选池
2随机选一个Math.random()
3更新 pickCount + lastPicked统计数据
4写入历史记录完整记录

两次 save 的必要性

awaitthis.saveFoods();// 保存 pickCount 变化awaitthis.saveHistory();// 保存新历史记录

pickCount 是食物的属性,存在 foods.json 里。历史记录存在 history.json 里。两个文件各自独立,需要分别保存。

history 记录的字段

{id:'h_'+Date.now(),// 唯一 ID:时间戳生成foodName:p.name,// 食物名称(冗余存储)emoji:p.emoji,// 表情(冗余存储)category:p.category,// 分类(冗余存储)pickedAt:Date.now()// 被选中时间}

history 里冗余存了 foodName 和 emoji——不存 foodId 引用。这是刻意的设计:如果食物被删除,历史记录仍然保留名字和表情。用引用的话,删除食物后历史记录就断链了。

六、历史记录的查询与清理

getHistory 方法

getHistory():PickRecord[]{returnthis.history.slice().sort((a:PickRecord,b:PickRecord)=>b.pickedAt-a.pickedAt);}

slice() 返回副本,sort() 排序——不修改原始数组。降序排列(b - a)——最新的在前面。每次调用都重新排序——保证数据一致性。

clearHistory 方法

asyncclearHistory():Promise<void>{this.history=[];awaitthis.saveHistory();}

直接清空数组,然后写入文件。没有确认弹窗——历史记录不包含关键数据(食物本身不受影响),清空后可以重新积累。

resetFoods 方法

asyncresetFoods():Promise<void>{this.foods=getDefaultFoods();awaitthis.saveFoods();}

恢复出厂设置——用默认数据替换当前数据。用户自己添加的食物全部丢失,但历史记录保留——resetFoods 不调用 clearHistory。

七、两种存储方案的统一

文件存储的部分

方法文件操作
saveFoods()foods.json全量写入
saveHistory()history.json全量写入
getFoods()内存读取slice/filter
getHistory()内存读取slice/sort

Preferences 存储的部分

方法操作
读主题‘theme’preferences.get
写主题‘theme’preferences.put + flush

食物和历史用文件,主题用 Preferences——各取所长。文件存大数组,Preferences 存小配置。两种方案在 Theme.ets 和 Index.ets 里分别调用,互不干扰。

八、性能考量

全量读写的代价

数据量文件大小读取耗时写入耗时
10 条食物~2KB<1ms<1ms
50 条食物~8KB<1ms<1ms
100 条食物~15KB<2ms<2ms
500 条历史~40KB<5ms<5ms

几十 KB 的 JSON 序列化在手机上几乎无感。如果数据量到几万条,才需要考虑增量写入或数据库方案。对于这个 App,全量写入完全够用。

内存 vs 文件的一致性

每次 CRUD 操作都是先改内存、再写文件。如果 App 被系统杀死(内存不足),最后一次写入前的数据会丢失。但这种情况极少——HarmonyOS 会尽量保活前台 App。

九、整体嵌套结构

FoodDatabase 数据库类 ├→ foods: FoodItem[] 内存中的食物数组 ├→ history: PickRecord[] 内存中的历史数组 ├→ fp / hp 文件路径 │ ├→ read / write 底层文件 I/O ├→ init() 初始化(读文件或用默认) │ ├→ getFoods / addFood 食物 CRUD ├→ deleteFood / toggleFav │ ├→ pick() 转盘抽签(最复杂) │ ├→ getHistory / clearHistory 历史管理 └→ resetFoods 重置数据

数据层是一个自包含的类——所有持久化逻辑都在这里面,UI 层不需要知道数据存在文件里还是 Preferences 里。Index.ets 调用 db.getFoods() 拿到数组,不关心数组是从 JSON 解析的还是从数据库查的。这就是封装的意义。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询