HarmonyOS RDB 关系设计:中式美食收藏和笔记为什么不要塞进同一个字段 中式美食的菜谱详情页有两个很容易被写到一起的东西收藏和笔记。页面上它们确实离得很近一个是点一下收藏另一个是写几句自己的口味调整。但我真落库的时候发现这两个东西不能混。收藏是用户的一个行为笔记是用户写下来的内容。取消收藏不应该把笔记删掉改笔记也不应该影响收藏列表的排序。这次主要解决什么问题大白话解释收藏和笔记是不是一张表不是。收藏是动作笔记是内容取消收藏要不要删除笔记不要。用户写过的内容应该留下改笔记要不要刷新收藏时间不要。否则收藏页排序会乱推荐排序能不能用这两个信号能但权重要分开当时如果偷懒会怎么写最省事的写法是做一个大对象里面什么都有。exportinterfaceRecipeUserState{recipeId:stringisFavorite:booleanfavoriteAt?:numbernoteText?:stringnoteUpdatedAt?:number}这个对象给页面用可以但不能直接当数据库结构。因为它把两种生命周期完全不同的数据塞到了一起。页面想展示时可以合并落库时必须拆开。我最后怎么拆表只管什么不管什么favorite_recipe这道菜有没有被收藏、什么时候收藏笔记正文recipe_note用户写了什么、什么时候改是否收藏CREATETABLEIFNOTEXISTSfavorite_recipe(recipe_idTEXTPRIMARYKEY,created_atINTEGERNOTNULL,source_pageTEXTNOTNULLDEFAULTdetail);CREATETABLEIFNOTEXISTSrecipe_note(recipe_idTEXTPRIMARYKEY,note_textTEXTNOTNULL,updated_atINTEGERNOTNULL,pinnedINTEGERNOTNULLDEFAULT0);这样拆完以后取消收藏就只删 favorite_recipe保存笔记就只改 recipe_note。两个动作互不误伤。ArkTS 层怎么写更清楚收藏仓储只处理收藏。exportclassFavoriteRepository{asyncsetFavorite(recipeId:string,favorite:boolean):Promisevoid{if(favorite){awaitthis.dao.upsertFavorite({recipeId,createdAt:Date.now(),sourcePage:detail})return}awaitthis.dao.deleteFavorite(recipeId)}}笔记仓储只处理笔记。exportclassRecipeNoteRepository{asyncsave(recipeId:string,text:string):Promisevoid{constnoteTexttext.trim()if(!noteText){awaitthis.dao.deleteNote(recipeId)return}awaitthis.dao.upsertNote({recipeId,noteText,updatedAt:Date.now(),pinned:0})}}详情页要用的时候再由 ViewModel 合并。exportclassRecipeDetailViewModel{asyncloadUserState(recipeId:string):PromiseRecipeUserState{constfavoriteawaitthis.favoriteRepository.find(recipeId)constnoteawaitthis.noteRepository.find(recipeId)return{recipeId,isFavorite:!!favorite,favoriteAt:favorite?.createdAt,noteText:note?.noteText,noteUpdatedAt:note?.updatedAt}}}环境和验证项目说明技术栈HarmonyOS、ArkTS、ArkUI、RDB relationalStore页面菜谱详情页、收藏页、我的厨房核心文件FavoriteRepository.ets、RecipeNoteRepository.ets、RecipeDetailViewModel.ets验收方式点收藏、写笔记、取消收藏、重新进入详情页操作预期结果收藏一道菜并写笔记收藏页有这道菜详情页能看到笔记取消收藏收藏页移除但笔记还在只写笔记不收藏我的厨房笔记入口能找到收藏页不出现修改笔记笔记时间更新收藏时间不变asyncfunctionverifyCancelFavoriteKeepsNote(){awaitfavoriteRepository.setFavorite(mapo-tofu,true)awaitnoteRepository.save(mapo-tofu,少放辣家里老人也能吃)awaitfavoriteRepository.setFavorite(mapo-tofu,false)conststateawaitdetailViewModel.loadUserState(mapo-tofu)assert(state.isFavoritefalse)assert(state.noteText少放辣家里老人也能吃)}总结一下这块不是为了把表设计得多复杂而是为了不让后面的功能互相踩。收藏、笔记、推荐、我的厨房都会读这部分数据。边界现在拆清楚后面加“常做菜”“带笔记的菜”“最近调整口味”这类功能时就不用再回头补救。我这里真正防的是误删这个设计看起来只是分两张表实际防的是一个很常见的坑用户点了取消收藏开发者顺手把“这道菜的用户状态”整条删掉。这样写代码最省事但用户之前写的笔记也没了。中式美食这种做饭类应用里笔记往往是用户自己的经验比如“少放盐”“孩子不吃辣”“下次换成鸡腿肉”。这些内容比收藏按钮本身更重要。我最后给自己定了一个很简单的规则只要是用户手写出来的内容就不能跟着一个按钮状态一起删除。按钮状态可以删用户内容要单独删而且必须是用户明确清空笔记。误写法问题deleteRecipeUserState(recipeId)收藏和笔记一起没了saveUserState({ isFavorite, noteText })改一个字段时容易覆盖另一个字段用 updatedAt 同时表示收藏和笔记更新时间排序会被污染asyncfunctionunsafeCancelFavorite(recipeId:string){// 这类写法要避免看似清理状态实际可能把笔记也清掉awaituserStateDao.deleteByRecipeId(recipeId)}正确做法是动作对动作内容对内容。asyncfunctionsafeCancelFavorite(recipeId:string){awaitfavoriteRepository.setFavorite(recipeId,false)// 不调用 noteRepository.delete}开发时怎么判断边界对不对我判断一张表该不该拆主要看三个问题。判断问题如果答案是“是”处理两个字段会不会独立删除会拆表两个字段排序规则是否不同不同拆表两个字段是否给不同业务使用是拆表收藏和笔记三个问题都命中所以不适合塞一起。收藏给收藏页、推荐排序、首页回流用笔记给我的厨房、详情页、私人记录用。它们只是经常在同一个页面出现不代表它们应该存成同一个东西。后续扩展会更轻分表以后后面要做“有笔记的菜”“最近修改过的菜”“收藏最多的分类”都很直接。SELECTrecipe_id,updated_atFROMrecipe_noteWHEREnote_textORDERBYupdated_atDESC;SELECTrecipe_id,created_atFROMfavorite_recipeORDERBYcreated_atDESC;这两条查询如果都压在一个混合 JSON 字段里就要先读出来再解析再排序。数据量小时能跑功能多了以后很难维护。排错时我会先看这几项收藏和笔记出了问题不要先去看页面样式先看数据是不是被写错。排查点看什么favorite_recipe 是否存在判断收藏按钮状态recipe_note 是否存在判断笔记是否应该回显created_at 是否变化判断收藏排序有没有被误刷新updated_at 是否变化判断笔记保存是否真的生效asyncfunctiondebugRecipeUserData(recipeId:string){constfavoriteawaitfavoriteRepository.find(recipeId)constnoteawaitnoteRepository.find(recipeId)console.info(favorite,favorite)console.info(note,note)}这个排查顺序很朴素但有效。先确认数据再看 ViewModel最后才看页面。否则很容易把一个落库边界问题误判成 ArkUI 渲染问题。