尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

搜索历史的记忆宫殿:ArkTS 为鸿蒙搜索框设计去重与上限的表

搜索历史的记忆宫殿:ArkTS 为鸿蒙搜索框设计去重与上限的表 实例搜索历史记录SearchHistory技术历史词表、唯一约束、SearchHistoryDao一、业务需求分析搜索历史的三个「怪癖」搜索历史是每个 App 都有的小功能看起来简单但它的数据行为有三个「怪癖」恰恰是数据库设计的考题去重同一个词搜三次历史里只显示一条但可以记录搜索次数——用户搜「HarmonyOS」反复搜说明这个词对他重要有上限历史不能无限累积一般保留最近 10~20 条超出要淘汰最久未搜的——这是 LIMIT 的运用场景时间排序最近搜过的排前面超时自动上浮——用户扫一眼历史就能点回刚才搜的词。这三个需求组合起来就是一个精妙的数据库题如何用一张表 唯一约束 LIMIT 删除实现「去重插入 超限淘汰 时间排序」。本实例就是这道题的完整解答。核心业务需求清单需求数据层实现页面呈现记录一次搜索插入/更新时间戳 次数标签流 时间线去重keyword 唯一约束同词只显示一条超限淘汰LIMIT 删除最旧保留 10 条一键清空DELETE 全表清空按钮单条删除DELETE by id长按删除二、字段设计一张极简的历史表搜索历史表 search_history字段名类型约束说明idINTEGERPRIMARY KEY AUTOINCREMENT自增主键keywordTEXTNOT NULL UNIQUE搜索词唯一约束去重核心search_timeINTEGERNOT NULL最近搜索时间戳毫秒countINTEGERNOT NULL DEFAULT 1累计搜索次数设计要点拆解1.keyword UNIQUE是去重的数据库级保证。表结构层面直接声明keyword TEXT NOT NULL UNIQUE——同一个词只能存在一行第二次搜索不会新增记录而是 UPDATE 这行的 search_time 和 count。去重逻辑交给数据库约束而非前端判断这是本实例的核心设计思想。2. search_time 每次搜索刷新。它记录「最近一次搜索的时间」排序用ORDER BY search_time DESC——最近搜的排最前。注意它不记录「第一次搜索时间」——历史列表只需要「最近」语义。3. count 累计搜索次数。同词反复搜索count 递增count 1。页面可以用它显示「HarmonyOS ×8」这种热度徽标暗示用户的搜索偏好。这个字段是可选的锦上添花但实现成本极低一次 UPDATE。4. 三字段极简设计。为什么没有「搜索类型」「来源页面」等扩展字段因为搜索历史的本质就是「词 时间 次数」加字段违反「单表职责单一」——复杂需求如按来源分组应拆表。能用 3 个字段表达的模型绝不用 6 个。三、建表 SQL唯一约束的两种写法CREATETABLEIFNOTEXISTSsearch_history(idINTEGERPRIMARYKEYAUTOINCREMENT,keywordTEXTNOTNULLUNIQUE,search_timeINTEGERNOTNULL,countINTEGERNOTNULLDEFAULT1);UNIQUE 约束的两种声明方式-- 方式一列级约束本实例采用keywordTEXTNOTNULLUNIQUE-- 方式二表级约束适合复合唯一UNIQUE(keyword)单列唯一用列级即可复合唯一如打卡实例的(habit_id, date)必须用表级。SQLite 的 UNIQUE 约束自动创建唯一索引——插入重复值会抛UNIQUE constraint failed异常这正是我们「先查后改」逻辑见下节的兜底防线。四、SearchHistoryDao 封装去重插入的核心方法数据层核心SearchHistoryDao。最关键的方法是addKeyword——它实现了「去重插入 时间刷新 次数累计 超限淘汰」四合一staticasyncaddKeyword(context:common.Context,keyword:string):Promisevoid{constkwkeyword.trim();if(!kw){return;}conststoreawaitSearchHistoryDao.getStore(context);// 1. 查重按词查是否已存在constprednewrelationalStore.RdbPredicates(SearchHistoryDao.TABLE);pred.equalTo(keyword,kw);constresultawaitstore.query(pred);if(result.goToNextRow()){// 已存在 → 更新时间 次数 1constidresult.getLong(result.getColumnIndex(id));constcountresult.getLong(result.getColumnIndex(count));result.close();constvalues:relationalStore.ValuesBucket{search_time:Date.now(),count:count1,};constupdatePrednewrelationalStore.RdbPredicates(SearchHistoryDao.TABLE);updatePred.equalTo(id,id);awaitstore.update(values,updatePred);}else{result.close();// 不存在 → 插入新记录constvalues:relationalStore.ValuesBucket{keyword:kw,search_time:Date.now(),count:1,};awaitstore.insert(SearchHistoryDao.TABLE,values);}// 2. 超限淘汰保留最近 MAX_KEEP 条awaitSearchHistoryDao.trim(context);}逻辑拆解第 1 步查重分支。equalTo(keyword, kw)查询——查到重复词走 UPDATE 分支刷新 search_time、count1查不到走 INSERT 分支新建 count1。这就是「去重插入」的完整语义不是简单地 INSERT而是「存在则更新、不存在则插入」UPSERT 语义。RDB 没有直接的 UPSERT 语法用「先查后改」实现逻辑清晰。第 2 步trim 超限淘汰。MAX_KEEP 10条上限超出的最旧记录被删除下节详解。关于result.close()的位置查重分支里UPDATE 前必须 close 结果集——虽然 RDB 通常允许查询后再写但规范做法是先释放游标再做写操作避免潜在的锁竞争。五、超限淘汰LIMIT 删除的 SQL 艺术trim方法实现「保留最近 MAX_KEEP 条删除更旧的」staticasynctrim(context:common.Context):Promisevoid{conststoreawaitSearchHistoryDao.getStore(context);// 1. 统计总数constcountResultawaitstore.querySql(SELECT COUNT(*) AS c FROM${SearchHistoryDao.TABLE});lettotal0;if(countResult.goToNextRow()){totalcountResult.getLong(countResult.getColumnIndex(c));}countResult.close();if(totalMAX_KEEP){return;// 未超限无需淘汰}// 2. 计算超出的条数constoverflowtotal-MAX_KEEP;// 3. 删除最旧的 overflow 条awaitstore.executeSql(DELETE FROM${SearchHistoryDao.TABLE}WHERE id IN ( SELECT id FROM${SearchHistoryDao.TABLE}ORDER BY search_time ASC LIMIT${overflow}));}这条 SQL 的精妙之处子查询选最旧ORDER BY search_time ASC LIMIT ${overflow}——按搜索时间升序最旧的在前取前 overflow 条就是「最久未搜的 overflow 个词」。外层按 id 删除DELETE ... WHERE id IN (子查询)——不能用DELETE ... ORDER BY ... LIMITSQLite 的 DELETE 不支持 LIMIT所以用子查询把「要删的 id 集合」算出来再按 id 删。这是 SQLite 删除 TOP-N 记录的标准写法。为什么不在 SQL 里一次算可以先查「总数 - MAX_KEEP」再决定是否删除——总数 上限时直接 return避免无谓的子查询。先判断再操作减少不必要的 SQL 执行。MAX_KEEP 常量const MAX_KEEP 10定义在 DAO 顶部是「保留条数」的单一事实来源。调整上限只改一处页面无需改动——这是常量提取的意义。六、基础查询与删除全部历史最近搜索在前staticasyncqueryAll(context:common.Context):PromiseSearchHistory[]{conststoreawaitSearchHistoryDao.getStore(context);constpredicatesnewrelationalStore.RdbPredicates(SearchHistoryDao.TABLE);predicates.orderByDesc(search_time);constresultawaitstore.query(predicates);returnSearchHistoryDao.collect(result);}orderByDesc(search_time)——最近搜的排最前这是历史列表的基本顺序。删除单条与一键清空staticasyncdeleteOne(context:common.Context,id:number):Promisenumber{conststoreawaitSearchHistoryDao.getStore(context);constpredicatesnewrelationalStore.RdbPredicates(SearchHistoryDao.TABLE);predicates.equalTo(id,id);returnawaitstore.delete(predicates);}staticasyncclearAll(context:common.Context):Promisenumber{conststoreawaitSearchHistoryDao.getStore(context);constpredicatesnewrelationalStore.RdbPredicates(SearchHistoryDao.TABLE);returnawaitstore.delete(predicates);}deleteOne删单条页面长按标签触发clearAll删全表页面「清空」按钮触发——注意store.delete的 predicates 不带条件时删除全部行返回受影响行数。七、技术要点对照表技术点实现方式生产价值去重keyword UNIQUE 约束数据库级防重复去重插入先查后改存在 UPDATE / 不存在 INSERTUPSERT 语义次数累计count 1热度徽标数据源超限淘汰DELETE … WHERE id IN (SELECT … LIMIT)TOP-N 删除标准写法时间排序orderByDesc(‘search_time’)最近优先一键清空predicates 无条件 delete全表清除八、文章小结搜索历史的数据层核心是**「唯一约束 先查后改 LIMIT 淘汰」**三件套UNIQUE 保证去重、先查后改实现 UPSERT、DELETE ... IN (SELECT ... LIMIT)实现超限淘汰。这是「有上限的键值记录」这类场景的标准建模——搜索历史、最近浏览、推荐记录都是同一模式。极简的 3 字段设计让模型一目了然。下一篇7-2展示极简标签流 UI——搜索框 历史标签 一键清空像热搜榜一样清爽。九、字段设计详解UNIQUE 去重、搜索次数与热度排序前文的三字段极简设计在实战中有三个细节值得深挖1. keyword UNIQUE 去重策略的边界。UNIQUE 约束区分大小写、也忽略不了空格——「HarmonyOS」与「harmonyos」是两条记录「 HarmonyOS」带空格与「HarmonyOS」也是两条。所以addKeyword第一步keyword.trim()不是可有可无输入层先归一化数据库约束才能发挥「同词只留一行」的威力。若还想大小写不敏感去重可在插入前统一toLowerCase()本实例保留原样尊重用户输入。2. count 是「搜索次数」的唯一事实来源。它由「存在则 count 1」维护永不手工赋值——避免并发写导致次数失真。页面徽标HarmonyOS 开发 ×8读的就是这个字段。3. 热度排序count 的另一半价值。时间排序search_time DESC回答「最近搜了什么」热度排序count DESC回答「我搜得最多的是什么」两种排序对应两种产品语义排序维度SQL 排序产品场景时间排序ORDER BY search_time DESC历史时间线默认热度排序ORDER BY count DESC我的热搜榜、Top 10// 按热度取 Top 10搜得最多的词排最前staticasyncqueryHot(context:common.Context,limit:number10):PromiseSearchHistory[]{conststoreawaitSearchHistoryDao.getStore(context);constpredicatesnewrelationalStore.RdbPredicates(SearchHistoryDao.TABLE);predicates.orderByDesc(count);predicates.limitAs(limit);constresultawaitstore.query(predicates);returnSearchHistoryDao.collect(result);}limitAs(limit)与 trim 里的LIMIT ${overflow}殊途同归——一个是查询侧取前 N一个是删除侧删前 NLIMIT 是 SQLite 控制「数量」的统一武器。十、热门搜索统计的 GROUP BY 思路细心的读者会发现search_history 表因为有 keyword UNIQUE 约束天然就是「每个词一行」——统计热门搜索根本不需要 GROUP BY直接 count 降序即可。那 GROUP BY 什么时候用得上答案是当「每次搜索」单独记日志时。假设升级需求「记录每个词每天被搜几次」就要拆一张日志表 search_log-- 每次搜索记一行不合并CREATETABLEIFNOTEXISTSsearch_log(idINTEGERPRIMARYKEYAUTOINCREMENT,keywordTEXTNOTNULL,search_timeINTEGERNOTNULL);热门统计就变成经典的聚合查询SELECTkeyword,COUNT(*)AScnt,MAX(search_time)ASlast_timeFROMsearch_logWHEREsearch_time${startTime}GROUPBYkeywordORDERBYcntDESCLIMIT10;GROUP BY keyword 把同一词的多次搜索聚合成一行COUNT(*) 算次数、MAX(search_time) 取最近时间。这就是单表 UNIQUE 换掉的复杂度——有唯一约束时 GROUP BY 是冗余的没有约束时才需要它。两套方案的选择标准历史只需要「最新状态」→ 单表 UNIQUE本实例历史需要「完整轨迹」→ 日志表 GROUP BY。十一、清空历史的 DELETE 操作细节clearAll一行store.delete(predicates)就把全表删光但生产环境有三个细节值得推敲1. DELETE 不重置自增主键。删光后再次插入id 会从 11 继续而不是从 1 开始。多数场景无感id 只做内部标识若产品要求「清空后重新编号」需手动重置DELETEFROMsqlite_sequenceWHEREnamesearch_history;2. 清空建议走事务。clearAll内部可包一层store.beginTransaction()配合「清空 灌种子数据」的连招7-4 的 initSeedData 就是先清后灌保证要么全成功要么全回滚避免半清空状态。3. 页面侧双重确认。清空是不可逆操作页面应弹 AlertDialog 确认防止误触——数据层的 DELETE 只管执行要不要删、删前是否确认是产品与 UI 层的责任。这条分层原则与「去重逻辑交给数据库」一脉相承数据层只提供能力不替业务做决策。十二、FAQ搜索历史表的高频疑问问题解答为什么用 UNIQUE 不用前端去重数据库约束是唯一可靠防线前端去重在多入口搜索框、热词点击时会漏INSERT OR REPLACE 能替代先查后改吗能去重但会删除旧行重建新行count 无法累计、id 会变化本场景不合适trim 为什么不用 DELETE … LIMITSQLite 的 DELETE 不支持 LIMIT必须用WHERE id IN (子查询)迂回时间戳为什么用毫秒Date.now() 直接可得与 RDB 的 INTEGER 兼容排序比较无精度损失搜索词需要单独建索引吗UNIQUE 约束自动创建唯一索引equalTo(‘keyword’) 查重已走索引无需重复建MAX_KEEP 改 20 会影响性能吗不会trim 只删超出的几条表数据量本身有上限性能恒定
返回列表