HarmonyOS 数据迁移实战|中式美食如何把收藏、浏览记录从 Preferences 迁到 RDB


中式美食早期把收藏、笔记、最近浏览放在 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 硬撑。

相关推荐
星释2 小时前
鸿蒙智能体开发实战:6.A2A 模式开发接口
华为·harmonyos·鸿蒙·智能体
澹然疏离2 小时前
HarmonyOS应用《民族图鉴》开发第34篇:TTSEngineService——语音引擎封装与降级策略
华为·harmonyos
花开彼岸天~3 小时前
03 module.json5 配置文件全解:能力声明、权限申请与设备支持
华为·harmonyos·鸿蒙系统
ZZZMMM.zip4 小时前
基于鸿蒙HarmonyOS NEXT开发AI股票分析应用:智能投资新体验与鸿蒙Flutter框架跨端实践
人工智能·flutter·华为·harmonyos·鸿蒙
花开彼岸天~5 小时前
04 oh-package.json5 与依赖管理:ohpm 包管理器全解析
华为·harmonyos·鸿蒙系统
爸爸6197 小时前
06 项目目录结构与编码规范:三层架构与 DAO 模式实践
华为·架构·harmonyos·鸿蒙系统
不才难以繁此生8 小时前
HarmonyOS 端侧应用实战:中式美食如何把资源、路由、存储和视频拆菜串成一条工程链路
harmonyos·arkts·arkui·中式美食·端侧应用
爸爸6198 小时前
07 ArkTS 基础类型与接口定义:从 TypeScript 到鸿蒙的类型升级
javascript·华为·typescript·harmonyos·鸿蒙系统
绝世番茄8 小时前
鸿蒙 HarmonyOS ArkTS 音乐播放器界面布局深度学习指南
深度学习·华为·list·harmonyos·鸿蒙