HarmonyOS RDB 收藏列表打开慢怎么办:中式美食怎么用批量查询避免循环查库
中式美食的收藏列表刚开始很顺,收藏三五道菜时感觉不到问题。等用户收藏了几十道菜,再打开“我的收藏”,页面开始慢半拍,甚至先空一下再出现卡片。这个时候不要先怪 ArkUI 列表,也不要急着给页面加骨架屏。先查 RDB 查询次数,很多卡顿都是一条一条查库查出来的。
慢在哪里
收藏列表通常有两张数据:一张是收藏关系,只存recipeId;另一张是菜谱表,存标题、封面、分类、步骤等内容。页面展示时,需要先知道用户收藏了哪些菜,再把这些 id 变成真正的菜谱卡片。
| 数据 | 适合存什么 | 不适合存什么 |
|---|---|---|
| favorite_recipe | recipeId、收藏时间 | 菜谱标题、封面、步骤大字段 |
| recipe | 菜谱完整信息 | 用户是否收藏这种个人状态 |
| 页面 ViewModel | 展示用字段 | 持久化真实来源 |
这个拆法本身没错。问题出在拿到favoriteIds后怎么查菜谱。如果每个 id 都单独查一次,收藏越多,查询次数就越多。为什么页面会慢?直接原因不是 RDB 不能查,而是你让它在一个页面打开动作里做了太多零碎小活。
坏写法:拿一个 id 查一次
下面这种代码看起来很直观:先查收藏 id,再循环查菜谱。
asyncfunctionloadFavoriteRecipesBad():Promise<RecipeCardVM[]>{constids=awaitFavoriteDao.listFavoriteRecipeIds()constresult:RecipeCardVM[]=[]for(constidofids){constrecipe=awaitRecipeDao.findById(id)if(recipe){result.push(toCardVM(recipe))}}returnresult}它的问题也很直观:收藏 5 条是 5 次查询,收藏 50 条就是 50 次查询。页面看起来只是打开一个列表,实际背后可能跑了几十次 SQL。这样写在调试数据少的时候没感觉,上线后用户数据一多就会露馅。
| 收藏数量 | 循环查库次数 | 页面风险 |
|---|---|---|
| 5 | 5 次 | 基本无感 |
| 30 | 30 次 | 中低端设备开始慢 |
| 80 | 80 次 | 页面首屏明显拖 |
| 150 | 150 次 | 容易出现空白和等待 |
这里不是说循环一定不能用,而是不能把数据库查询放进这种无边界的循环里。循环处理内存数组可以,循环访问 RDB 就要谨慎。
好写法:一次把收藏菜谱拿回来
更稳的方式是:先拿收藏 id,再用IN条件一次查回菜谱。
asyncfunctionloadFavoriteRecipes():Promise<RecipeCardVM[]>{constids=awaitFavoriteDao.listFavoriteRecipeIds()if(ids.length===0){return[]}constrecipes=awaitRecipeDao.listByIds(ids)constrecipeMap=newMap<string,Recipe>()recipes.forEach((item)=>recipeMap.set(item.id,item))returnids.map((id)=>recipeMap.get(id)).filter((item):itemisRecipe=>!!item).map((item)=>toCardVM(item))}这段代码有两个关键点。第一,RDB 查询次数稳定,不会随着收藏数量线性增加。第二,最后用ids.map()还原顺序,保证列表顺序跟收藏时间一致。很多人做批量查询后会忽略顺序,结果页面排序突然乱了。
DAO 里怎么写
DAO 层不要把 SQL 拼得太随意。实际项目里可以按你使用的 RDB API 组织参数,重点是把参数化、空数组、数量上限处理好。
classRecipeDao{asynclistByIds(ids:string[]):Promise<Recipe[]>{if(ids.length===0){return[]}constholders=ids.map(()=>'?').join(',')constsql=`SELECT id, title, cover, category, cook_time, difficulty FROM recipe WHERE id IN (${holders})`constresultSet=awaitthis.rdb.querySql(sql,ids)returnthis.readRecipes(resultSet)}privatereadRecipes(resultSet:relationalStore.ResultSet):Recipe[]{constrows:Recipe[]=[]while(resultSet.goToNextRow()){rows.push({id:resultSet.getString(resultSet.getColumnIndex('id')),title:resultSet.getString(resultSet.getColumnIndex('title')),cover:resultSet.getString(resultSet.getColumnIndex('cover')),category:resultSet.getString(resultSet.getColumnIndex('category')),cookTime:resultSet.getLong(resultSet.getColumnIndex('cook_time')),difficulty:resultSet.getString(resultSet.getColumnIndex('difficulty')),})}resultSet.close()returnrows}}这里还有一个小点:收藏列表首屏不一定需要菜谱步骤、营养说明、长文本介绍。列表卡片用什么字段,就只查什么字段。详情页需要完整内容时,再按 id 查完整菜谱。这样能少搬很多没必要的数据。
id 太多时怎么处理
如果用户收藏特别多,IN条件也不能无限塞。中式美食这种应用一般收藏数量不会夸张,但代码还是要留边界。可以按页取收藏 id,再批量查这一页对应的菜谱。
asyncfunctionloadFavoritePage(page:number,pageSize:number):Promise<RecipeCardVM[]>{constids=awaitFavoriteDao.listFavoriteRecipeIds(page,pageSize)if(ids.length===0){return[]}constrecipes=awaitRecipeDao.listByIds(ids)constmap=newMap<string,Recipe>()recipes.forEach((item)=>map.set(item.id,item))returnids.map((id)=>map.get(id)).filter((item):itemisRecipe=>!!item).map((item)=>toCardVM(item))}这样页面第一次打开只拿第一页,用户继续往下滑再拿下一页。解决慢的问题,不是把所有东西一次塞给页面,而是让首屏先稳住。
| 做法 | 查询次数 | 首屏压力 | 适合场景 |
|---|---|---|---|
| 每个 id 查一次 | 多 | 高 | 不建议 |
全量IN查询 | 少 | 中 | 收藏数量不大 |
| 分页 id + 批量查询 | 少且可控 | 低 | 收藏数量可能变多 |
页面层别做数据库细节
ArkUI 页面层最好只关心“我要收藏列表”。数据库怎么查、怎么分批、怎么还原顺序,放到 Repository 里。页面层拿到 ViewModel 后负责展示、空状态和点击跳转。
@StatefavoriteRecipes:RecipeCardVM[]=[]@Stateloading:boolean=false@Stateempty:boolean=falseasyncaboutToAppear(){this.loading=truetry{this.favoriteRecipes=awaitFavoriteRecipeRepository.instance.loadFirstPage()this.empty=this.favoriteRecipes.length===0}finally{this.loading=false}}这样以后要改 RDB 查询、加缓存、加分页,都不用改页面结构。页面只是根据loading、empty、favoriteRecipes展示不同状态。
怎么验证是真的变快
不要只凭感觉说变快了。至少做几组数据看。
| 验证项 | 具体做法 | 期待结果 |
|---|---|---|
| 查询次数 | 打日志记录打开收藏列表时的 SQL 次数 | 从 N 次变成 1 到 2 次 |
| 首屏耗时 | 记录点击到首屏出现卡片的时间 | 收藏 50 条也稳定 |
| 顺序 | 按收藏时间倒序检查列表 | 批量查询后顺序不乱 |
| 空状态 | 没有收藏时打开页面 | 直接显示空状态,不查菜谱表 |
| 删除收藏 | 收藏页取消一条后刷新 | 卡片移除,数量更新 |
我会特别看“顺序”和“空状态”。很多性能改完后,查询次数少了,但列表顺序乱了;或者空收藏还去查菜谱表。这些都说明边界没有收干净。
以后怎么避免
以后写这类页面,先问三个问题。
第一,这个页面是不是先拿一批 id,再拿详情数据?如果是,就优先考虑批量查询。
第二,列表卡片是不是真的需要完整表字段?如果只展示标题、封面、分类,就不要把长文本和步骤一起查出来。
第三,页面排序是谁决定的?如果收藏时间决定顺序,就用收藏 id 列表的顺序还原,不要让数据库返回顺序随缘。
中式美食的收藏列表、最近浏览、购物清单其实都是同一种问题:先有一张关系表,再回到菜谱表拿展示数据。把这条链路写稳以后,后面做浏览记录、买菜清单、推荐回放,都能复用同一套思路。不要让一个列表打开动作偷偷跑几十次查询,这是 RDB 页面性能最容易被忽略的坑。