资讯动态

把心事存进鸿蒙:ArkTS 为日记本设计长文本表与时间戳字段

发布时间:2026/8/16 3:51:15 来源:尧图企业网站定制
实例电子日记本Diary技术长文本存储、时间分组查询、关键词搜索一、业务需求分析日记本的数据形态与记账本有何不同前三个实例我们处理了「任务清单」短文本状态、「通讯录」结构化字段和「记账本」数值时间。到了实例 4「电子日记本」数据形态发生了一次质变主体内容从「字段」变成了「长文本」。日记应用的核心业务需求可以归纳为五点写日记标题 正文可能上千字的长文本 心情 天气 地点一次保存按时间轴浏览日记天生是「按时间组织的个人编年史」页面要按日期/月份分组展示时间线是主视觉关键词搜索用户想「找那天写日记提到爬山的记录」需要标题/正文双字段模糊搜索编辑与删除日记写错了要能改、能删轻量统计总篇数、本月篇数支撑页面头部信息。对比记账本日记本的数据特点维度记账本实例 3日记本实例 4主数据金额 REAL 分类 TEXT正文长文本 TEXT时间角色统计维度SUM/GROUP BY组织维度时间轴分组检索方式分类精确匹配关键词 LIKE 模糊更新频率几乎不更新删了重记经常编辑改日记这些差异决定了表结构设计的不同侧重。下面进入字段设计。二、字段设计表每一列都为「时间轴」服务字段名类型约束说明idINTEGERPRIMARY KEY AUTOINCREMENT自增主键titleTEXTNOT NULL标题contentTEXTNOT NULL正文长文本可数千字moodTEXTDEFAULT ‘’心情 emoji…weatherTEXTDEFAULT ‘’天气晴/多云/雨…locationTEXTDEFAULT ‘’地点created_timeINTEGERNOT NULL创建时间戳毫秒updated_timeINTEGERNOT NULL最近修改时间戳毫秒设计要点逐条拆解1. content 用 TEXT 存长文本不需要分表。这是初学者最容易纠结的问题「正文那么长要不要单独建一张 content 表」答案是不需要。SQLite 单条 TEXT 字段可以存数百 MB一篇几千字的日记约 10KB毫无压力。RDB 的设计原则是「一行一个业务实体」——一篇日记就是一行正文是它的一个属性。强行拆表反而增加 JOIN 复杂度得不偿失。2. mood 直接存 emoji 字符。心情用 emoji//而非数字编码。为什么这里不用记账本 type 的 0/1 数字编码因为心情没有「参与计算」的需求——不做 SUM、不做 GROUP BY 统计只有展示。当枚举值只用于展示时直接存可读文本UI 零转换是最务实的方案。判断用数字还是文本的标准这个字段会不会参与聚合运算会如记账本 type就编码成数字不会如心情就存文本。3. location 取代「标签」体系。早期的文章版设计里有tags逗号分隔标签字段落地版将其替换为location地点。原因个人日记场景下「标签」概念偏重而「地点」新家/公司/书房更贴近日记的自然属性也与文章版 4-2 的时间轴节点设计一致。如果你需要标签体系逗号分隔 LIKE 查询的模式可以随时加回来——SQLite 对字符串的处理非常灵活。4. created_time 与 updated_time 双时间戳。这是内容型应用的标准配置created_time创建时刻永不变更updated_time每次编辑刷新时间轴排序用它——「最近修改的日记排前面」比「最早写的排前面」更符合用户心智刚编辑完的日记大概率还想继续看。5. 为什么只给 created_time 建索引落地版的建表 SQL 为created_time建了idx_diary_time索引排序、按月份LIKE 2025-06%过滤都走它。其实更严谨的做法是索引updated_time排序字段但个人日记几百条数据两者性能差异不可感知选择 created_time 是因为它的值域稳定插入后不变索引不会因频繁更新而失效。这个细节体现了「索引服务于查询」的朴素原则。三、建表 SQL把设计变成现实CREATETABLEIFNOTEXISTSdiary(idINTEGERPRIMARYKEYAUTOINCREMENT,titleTEXTNOTNULL,contentTEXTNOTNULL,moodTEXTDEFAULT,weatherTEXTDEFAULT,locationTEXTDEFAULT,created_timeINTEGERNOTNULL,updated_timeINTEGERNOTNULL);CREATEINDEXIFNOTEXISTSidx_diary_timeONdiary(created_time);注意三点IF NOT EXISTS幂等重复执行不报错这是 getStore 里建表语句的统一要求DEFAULT mood 有默认值插入时可以省略代码更简洁NOT NULL DEFAULT ‘’weather、location 可空但默认空串避免 NULL 泄漏到 UI 层页面|| 兜底是双保险。四、DiaryDao 封装把 SQL 关进类里数据层核心是DiaryDao职责边界与前面实例一致只管数据库不管 UI。先看实体接口exportinterfaceDiary{id:number;title:string;content:string;// 长文本正文mood:string;// 心情 emoji…weather:string;// 天气晴/多云/雨…location:string;// 地点createdTime:number;// 日记时间戳毫秒updatedTime:number;}字段命名依然是「DB 蛇形 ↔ 代码驼峰」的双风格映射集中在rowToDiary方法privatestaticrowToDiary(result:relationalStore.ResultSet):Diary{return{id:result.getLong(result.getColumnIndex(id)),title:result.getString(result.getColumnIndex(title)),content:result.getString(result.getColumnIndex(content)),mood:result.getString(result.getColumnIndex(mood))||,weather:result.getString(result.getColumnIndex(weather))||,location:result.getString(result.getColumnIndex(location))||,createdTime:result.getLong(result.getColumnIndex(created_time)),updatedTime:result.getLong(result.getColumnIndex(updated_time)),};}与文章版代码的关键差异这是本实例读者最容易踩的坑文章版用row: relationalStore.ValuesBucketrow.id as number强转落地版改用ResultSetgetColumnIndexgetLong/getString——前者拿到的getRow()是弱类型的键值容器强转不安全后者类型安全、NULL 可兜底。文章版静态方法里写this.store落地版一律DiaryDao.store——ArkTS 禁止静态上下文使用thisarkts-no-this-in-static。文章版 context 用common.UIAbilityContext落地版统一common.Context基类复用性更强。getStore单例复用模式不变staticasyncgetStore(context:common.Context):PromiserelationalStore.RdbStore{if(DiaryDao.store){returnDiaryDao.store;}constconfig:relationalStore.StoreConfig{name:diary.db,securityLevel:relationalStore.SecurityLevel.S1,};DiaryDao.storeawaitrelationalStore.getRdbStore(context,config);// 建表 建索引见上节 SQLhilog.info(DOMAIN,TAG,日记表初始化成功);returnDiaryDao.store;}五、核心 CRUD写、查、改、删插入写日记staticasyncinsert(context:common.Context,d:Diary):Promisenumber{conststoreawaitDiaryDao.getStore(context);constvalues:relationalStore.ValuesBucket{title:d.title,content:d.content,mood:d.mood,weather:d.weather,location:d.location,created_time:d.createdTime,updated_time:d.updatedTime,};returnawaitstore.insert(DiaryDao.TABLE,values);}注意content作为字符串直接塞进 ValuesBucket——长文本无需特殊处理SQLite 自动分配存储这是 TEXT 类型的天然优势。全部日记按修改时间倒序staticasyncqueryAll(context:common.Context):PromiseDiary[]{conststoreawaitDiaryDao.getStore(context);constpredicatesnewrelationalStore.RdbPredicates(DiaryDao.TABLE);predicates.orderByDesc(created_time);constresultawaitstore.query(predicates);returnDiaryDao.collect(result);}排序字段用created_time与索引一致页面时间轴按创建时间分组展示语义上更符合「日记编年史」。更新编辑日记刷新 updated_timestaticasyncupdate(context:common.Context,d:Diary):Promisenumber{conststoreawaitDiaryDao.getStore(context);constvalues:relationalStore.ValuesBucket{title:d.title,content:d.content,mood:d.mood,weather:d.weather,location:d.location,updated_time:d.updatedTime,};constpredicatesnewrelationalStore.RdbPredicates(DiaryDao.TABLE);predicates.equalTo(id,d.id);returnawaitstore.update(values,predicates);}注意 update 不更新created_time——创建时间不可变只刷updated_time这是双时间戳的协作方式。删除与计数staticasyncdelete(context:common.Context,id:number):Promisenumber{conststoreawaitDiaryDao.getStore(context);constpredicatesnewrelationalStore.RdbPredicates(DiaryDao.TABLE);predicates.equalTo(id,id);returnawaitstore.delete(predicates);}staticasynccount(context:common.Context):Promisenumber{conststoreawaitDiaryDao.getStore(context);constresultawaitstore.querySql(SELECT COUNT(*) AS c FROM${DiaryDao.TABLE});lettotal0;if(result.goToNextRow()){totalresult.getLong(result.getColumnIndex(c));}result.close();returntotal;}六、时间分组查询按月份捞日记时间轴页面的核心需求选中某个月份只看那个月的日记。落地版提供queryByMonthstaticasyncqueryByMonth(context:common.Context,month:string):PromiseDiary[]{conststoreawaitDiaryDao.getStore(context);constpredicatesnewrelationalStore.RdbPredicates(DiaryDao.TABLE);predicates.like(created_time,${month}%).orderByDesc(created_time);constresultawaitstore.query(predicates);returnDiaryDao.collect(result);}这个方法的巧妙之处like(created_time, 2025-06%)用 LIKE 前缀匹配模拟「时间范围查询」。因为 created_time 存的是毫秒时间戳的字符串形式——不对等等created_time 是 INTEGER 类型LIKE 匹配的是数字的字符串表示。2025-06-01 00:00:00的时间戳是1748707200000用LIKE 2025-06%匹配不到这是一个需要澄清的坑文章版的queryByMonth按updated_time LIKE的设计在 INTEGER 时间戳上是不成立的那是按文本日期存储时的写法。落地版页面DiaryPage采用更可靠的方式直接queryAll拉全量在内存里按fmtDate分组渲染时间轴节点。个人日记数据量小几十上百篇全量加载 前端分组是务实的选择若数据量大正确做法是between(startOfMonth, endOfMonth)毫秒区间查询——这与记账本实例的区间查询一脉相承。关键词搜索标题/正文双字段 LIKEstaticasyncsearch(context:common.Context,keyword:string):PromiseDiary[]{conststoreawaitDiaryDao.getStore(context);constpredicatesnewrelationalStore.RdbPredicates(DiaryDao.TABLE);predicates.like(title,%${keyword}%).or().like(content,%${keyword}%).orderByDesc(created_time);constresultawaitstore.query(predicates);returnDiaryDao.collect(result);}LIKE %关键词%是包含匹配——标题或正文任意位置出现关键词即命中。.or()把两个条件连接为 OR 语义。注意LIKE 前导%会导致索引失效无法走 B 树全表扫描但日记本几百条数据扫描无压力。如果将来数据量到十万级应改用 FTS5 全文索引——那是另一个维度的优化本书暂不展开。七、技术要点对照表技术点实现方式生产价值长文本TEXT 字段直接存正文无需分表读写简单双时间戳created_time updated_time创建/编辑分离时间轴按编辑排序关键词搜索title/content OR LIKE双字段命中率高时间分组全量加载 前端按日期分组小数据量下的务实方案emoji 存储mood 直接存字符UI 零转换幂等建表CREATE IF NOT EXISTS重复启动不报错八、文章小结日记本数据层与记账本的数值聚合完全不同重心在长文本存储策略 双时间戳的时间轴组织 LIKE 关键词检索。TEXT 类型让长正文毫无负担双时间戳让「最近编辑优先」成为可能LIKE 让搜索零配置。下一篇4-2将展示这些数据如何被时间轴 UI 呈现——垂直时间轴 日期节点 心情天气卡片把数据变成故事。动手练习尝试给search增加第三个条件按天气过滤equalTo(weather, 晴)并观察 OR 与 AND 组合时 RdbPredicates 的链式写法。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价