
中式美食早期把收藏、笔记、最近浏览放在 Preferences 里,这个选择没问题。Preferences 很轻,适合保存少量 key-value 状态,开发也快。
但应用继续往前走以后,数据用途变多了。收藏不只是详情页的一个红心,它会影响收藏页、推荐排序、最近浏览、购物清单转化。最近浏览也不只是一个历史列表,它能告诉推荐页用户最近在关注什么。这个时候继续把所有行为数据塞在 Preferences 里,就会越来越吃力。
这篇讲的不是"Preferences 不好",而是中式美食什么时候该从 Preferences 迁到 RDB,以及迁移时怎么保证用户数据不丢、页面不用跟着大改。
本章位置
| 项目 | 内容 |
|---|---|
| 应用名称 | 中式美食 |
| 原始存储 | Preferences |
| 目标存储 | RDB / relationalStore |
| 涉及数据 | 收藏、笔记、最近浏览、搜索历史、推荐行为信号 |
| 关键目标 | 数据可查询、可迁移、可回滚、页面无感知 |
本文导读
| 问题 | 做法 |
|---|---|
| Preferences 还能不能继续用 | 可以,但只适合轻量配置和少量状态 |
| 为什么迁到 RDB | 行为数据需要筛选、排序、聚合、推荐计算 |
| 迁移怎么避免丢数据 | 先读旧数据,写新表,校验数量,再标记迁移完成 |
| 页面要不要关心迁移 | 不要,页面只调用 Repository |
| 出错怎么办 | 不删除旧数据,迁移状态保留为 pending 或 failed |
什么时候 Preferences 不够用了
Preferences 最适合保存简单状态,比如主题模式、是否看过引导、用户默认筛选项。这些数据通常只有一个 key,一个 value,不需要复杂查询。
但收藏和浏览记录不太一样。它们后面会变成行为数据。
| 数据 | 早期用法 | 后续用法 |
|---|---|---|
| 收藏菜谱 | 判断详情页红心是否点亮 | 收藏页排序、推荐偏好、分类统计 |
| 最近浏览 | 展示历史访问 | 推荐排序、继续浏览、重复曝光控制 |
| 搜索历史 | 下次搜索补全 | 用户近期意图、推荐关键词加权 |
| 购物清单 | 临时保存食材 | 推荐食材命中、采购计划分析 |
当数据只用于当前页面,Preferences 够用。当数据开始被多个页面、多种算法、多条链路复用,就应该考虑 RDB。
迁移的边界放在 Repository
页面层不要知道数据正在迁移。页面只应该调用仓储接口:
ts
class FavoriteRepository {
async listFavorites(): Promise<FavoriteRecord[]> {
await this.migration.ensureMigrated()
return this.rdbFavoriteDao.findAll()
}
async toggleFavorite(recipeId: string): Promise<void> {
await this.migration.ensureMigrated()
await this.rdbFavoriteDao.toggle(recipeId)
}
}
这样做的好处是,详情页、收藏页、推荐页都不用改。它们仍然调用 FavoriteRepository。迁移发生在仓储内部,页面只拿到最终结果。
如果把迁移逻辑写进页面,后面会很麻烦。收藏页要迁一次,详情页也要迁一次,推荐页还要判断是不是迁完了。这样很容易出现某个页面漏处理。
迁移状态要单独记录
迁移不能靠"感觉已经迁过了"。要有明确状态。
ts
type MigrationStatus = 'pending' | 'running' | 'done' | 'failed'
type MigrationMeta = {
name: string
fromVersion: number
toVersion: number
status: MigrationStatus
migratedAt?: number
errorMessage?: string
}
中式美食可以先做一个本地迁移记录表,比如 migration_meta。每次启动仓储前先看这张表:
| 状态 | 处理方式 |
|---|---|
| pending | 执行迁移 |
| running | 上次可能异常中断,重新校验后继续 |
| done | 不再重复迁移 |
| failed | 保留旧数据,记录错误,等待下一次修复 |
不要只在内存里记一个布尔值。App 退出、崩溃、升级之后,内存状态就没了。迁移状态本身也应该持久化。
迁移过程分四步
我会把迁移拆成四步,而不是一口气读旧数据、删旧数据、写新数据。
| 步骤 | 动作 | 目的 |
|---|---|---|
| 1 | 读取 Preferences 旧数据 | 保证源数据还在 |
| 2 | 写入 RDB 新表 | 让新查询链路可用 |
| 3 | 校验数量和关键字段 | 确认没有明显丢失 |
| 4 | 标记迁移完成 | 后续不再重复执行 |
示例代码可以这样组织:
ts
class FavoriteMigration {
async ensureMigrated(): Promise<void> {
const meta = await this.metaDao.find('favorite_preferences_to_rdb')
if (meta?.status === 'done') {
return
}
await this.metaDao.markRunning('favorite_preferences_to_rdb')
const oldItems = await this.preferencesFavoriteStore.readAll()
const records = oldItems.map(item => ({
recipeId: item.recipeId,
createdAt: item.createdAt ?? Date.now(),
source: 'preferences'
}))
await this.rdbFavoriteDao.upsertMany(records)
const count = await this.rdbFavoriteDao.count()
if (count < records.length) {
await this.metaDao.markFailed('favorite_preferences_to_rdb', 'favorite count mismatch')
return
}
await this.metaDao.markDone('favorite_preferences_to_rdb')
}
}
这里最重要的是:迁移成功之前不要删除旧数据。
如果写 RDB 中途失败,旧 Preferences 还在,下次可以继续迁。对用户来说,最坏情况是新能力暂时没启用,而不是收藏记录直接丢了。
RDB 表不要只存一个 id
收藏表如果只存 recipeId,短期也能用,但后面会不够。
中式美食推荐排序需要知道用户什么时候收藏、从哪里收藏、是否从搜索页进入。收藏页也可能需要按时间排序。
更合理的表字段可以是:
| 字段 | 作用 |
|---|---|
| recipe_id | 菜谱 id,核心业务标识 |
| created_at | 收藏时间,用于排序和推荐衰减 |
| source | 收藏来源,比如 detail/search/recommend |
| note_count | 是否有关联笔记,可以后续补 |
| updated_at | 后续同步或修正时使用 |
这样不是为了过度设计,而是为了让行为数据后面能被复用。
页面层保持无感知
页面层应该看到稳定接口,而不是看到迁移细节。
ts
@Entry
@Component
struct FavoritePage {
@State items: RecipeListItem[] = []
async aboutToAppear() {
this.items = await favoriteViewModel.loadFavoriteList()
}
build() {
List() {
ForEach(this.items, (item: RecipeListItem) => {
ListItem() {
RecipeCard({ item })
}
}, (item: RecipeListItem) => item.id)
}
}
}
这里页面只关心 loadFavoriteList()。至于数据是从 Preferences 来,还是从 RDB 来,页面不需要知道。
这个边界很关键。迁移不是 UI 问题,而是本地数据层问题。页面感知越少,后面改动越安全。
迁移后的验证要具体
数据迁移不能只看"没有报错"。至少要做几组实际动作。
| 验证动作 | 预期结果 |
|---|---|
| 升级前收藏 5 道菜,升级后打开收藏页 | 仍然显示 5 道菜 |
| 收藏页按时间排序 | 新旧收藏都能按 created_at 排序 |
| 详情页取消收藏 | RDB 状态更新,列表页同步变化 |
| 搜索页收藏一道菜 | source 能记录为 search |
| 推荐页读取收藏偏好 | 能从 RDB 查询到收藏集合 |
| 迁移中断后重启 | 不丢旧数据,可以继续迁移或重新校验 |
这类验证比"页面能打开"更有价值。因为迁移真正怕的是隐性丢数据:页面能打开,但收藏少了几条;推荐能跑,但历史行为没迁过来。
推荐阅读
| 主题 | 为什么建议一起看 | 链接 |
|---|---|---|
| Preferences 仓储封装 | 这是迁移前的旧存储基础 | 阅读文章 |
| 用户行为数据 | 收藏、浏览和搜索会成为推荐信号 | 阅读文章 |
| RDB 菜谱表设计 | RDB 表结构会决定后续查询能力 | 阅读文章 |
| 推荐排序 | 迁移后的行为数据可以服务推荐分数 | 阅读文章 |
本章小结
中式美食从 Preferences 迁到 RDB,不是为了显得架构更重,而是因为收藏、浏览、搜索这些数据已经不只是页面状态了。它们会影响收藏页、搜索体验、推荐排序和购物清单转化。
我的判断是:轻量配置继续留在 Preferences,真正会被查询、排序、聚合、推荐使用的行为数据迁到 RDB。迁移逻辑放在 Repository 内部,页面保持无感知;迁移成功前不删除旧数据;迁移状态单独记录;最后用具体动作验证数据没有丢。
最后留一个实际问题:如果你的 HarmonyOS 应用早期也用了 Preferences 保存业务数据,现在有没有哪个数据已经开始被多个页面复用?如果有,这类数据就应该进入迁移清单,而不是继续靠 key-value 硬撑。