当前位置: 首页 > news >正文

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


实例:电子日记本(Diary)|技术:长文本存储、时间分组查询、关键词搜索

一、业务需求分析:日记本的数据形态与记账本有何不同

前三个实例我们处理了「任务清单」(短文本+状态)、「通讯录」(结构化字段)和「记账本」(数值+时间)。到了实例 4「电子日记本」,数据形态发生了一次质变:主体内容从「字段」变成了「长文本」

日记应用的核心业务需求可以归纳为五点:

  1. 写日记:标题 + 正文(可能上千字的长文本)+ 心情 + 天气 + 地点,一次保存;
  2. 按时间轴浏览:日记天生是「按时间组织的个人编年史」,页面要按日期/月份分组展示,时间线是主视觉;
  3. 关键词搜索:用户想「找那天写日记提到爬山的记录」,需要标题/正文双字段模糊搜索;
  4. 编辑与删除:日记写错了要能改、能删;
  5. 轻量统计:总篇数、本月篇数,支撑页面头部信息。

对比记账本,日记本的数据特点:

维度记账本(实例 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);

注意三点:

  1. IF NOT EXISTS幂等:重复执行不报错,这是 getStore 里建表语句的统一要求;
  2. DEFAULT '😀':mood 有默认值,插入时可以省略,代码更简洁;
  3. 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')),};}

与文章版代码的关键差异(这是本实例读者最容易踩的坑):

  1. 文章版用row: relationalStore.ValuesBucket+row.id as number强转;落地版改用ResultSet+getColumnIndex+getLong/getString——前者拿到的getRow()是弱类型的键值容器,强转不安全;后者类型安全、NULL 可兜底。
  2. 文章版静态方法里写this.store;落地版一律DiaryDao.store——ArkTS 禁止静态上下文使用thisarkts-no-this-in-static)。
  3. 文章版 context 用common.UIAbilityContext;落地版统一common.Context基类,复用性更强。

getStore单例复用模式不变:

staticasyncgetStore(context:common.Context):Promise<relationalStore.RdbStore>{if(DiaryDao.store){returnDiaryDao.store;}constconfig:relationalStore.StoreConfig={name:'diary.db',securityLevel:relationalStore.SecurityLevel.S1,};DiaryDao.store=awaitrelationalStore.getRdbStore(context,config);// 建表 + 建索引(见上节 SQL)hilog.info(DOMAIN,TAG,'日记表初始化成功');returnDiaryDao.store;}

五、核心 CRUD:写、查、改、删

插入(写日记):

staticasyncinsert(context:common.Context,d:Diary):Promise<number>{conststore=awaitDiaryDao.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):Promise<Diary[]>{conststore=awaitDiaryDao.getStore(context);constpredicates=newrelationalStore.RdbPredicates(DiaryDao.TABLE);predicates.orderByDesc('created_time');constresult=awaitstore.query(predicates);returnDiaryDao.collect(result);}

排序字段用created_time(与索引一致),页面时间轴按创建时间分组展示,语义上更符合「日记编年史」。

更新(编辑日记,刷新 updated_time):

staticasyncupdate(context:common.Context,d:Diary):Promise<number>{conststore=awaitDiaryDao.getStore(context);constvalues:relationalStore.ValuesBucket={title:d.title,content:d.content,mood:d.mood,weather:d.weather,location:d.location,updated_time:d.updatedTime,};constpredicates=newrelationalStore.RdbPredicates(DiaryDao.TABLE);predicates.equalTo('id',d.id);returnawaitstore.update(values,predicates);}

注意 update 不更新created_time——创建时间不可变,只刷updated_time,这是双时间戳的协作方式。

删除计数

staticasyncdelete(context:common.Context,id:number):Promise<number>{conststore=awaitDiaryDao.getStore(context);constpredicates=newrelationalStore.RdbPredicates(DiaryDao.TABLE);predicates.equalTo('id',id);returnawaitstore.delete(predicates);}staticasynccount(context:common.Context):Promise<number>{conststore=awaitDiaryDao.getStore(context);constresult=awaitstore.querySql(`SELECT COUNT(*) AS c FROM${DiaryDao.TABLE}`);lettotal=0;if(result.goToNextRow()){total=result.getLong(result.getColumnIndex('c'));}result.close();returntotal;}

六、时间分组查询:按月份捞日记

时间轴页面的核心需求:选中某个月份,只看那个月的日记。落地版提供queryByMonth

staticasyncqueryByMonth(context:common.Context,month:string):Promise<Diary[]>{conststore=awaitDiaryDao.getStore(context);constpredicates=newrelationalStore.RdbPredicates(DiaryDao.TABLE);predicates.like('created_time',`${month}%`).orderByDesc('created_time');constresult=awaitstore.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%'匹配不到!

这是一个需要澄清的坑:文章版的queryByMonthupdated_time LIKE的设计在 INTEGER 时间戳上是不成立的(那是按文本日期存储时的写法)。落地版页面(DiaryPage)采用更可靠的方式:直接queryAll拉全量,在内存里按fmtDate分组渲染时间轴节点。个人日记数据量小(几十上百篇),全量加载 + 前端分组是务实的选择;若数据量大,正确做法是between(startOfMonth, endOfMonth)毫秒区间查询——这与记账本实例的区间查询一脉相承。

关键词搜索(标题/正文双字段 LIKE):

staticasyncsearch(context:common.Context,keyword:string):Promise<Diary[]>{conststore=awaitDiaryDao.getStore(context);constpredicates=newrelationalStore.RdbPredicates(DiaryDao.TABLE);predicates.like('title',`%${keyword}%`).or().like('content',`%${keyword}%`).orderByDesc('created_time');constresult=awaitstore.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 的链式写法。

http://www.jsqmd.com/news/1402254/

相关文章:

  • 考试安排问答智能体系统
  • PyCharm新手入门:从零安装到第一个Python项目实战
  • 2026重庆兴星铝材批发价格透明避坑指南,实力测评口碑推荐 - mypinpai
  • 2026 SERP API 横评:入门成本、单价、积分规则,6 家一次看完
  • 实战指南:基于沙箱环境构建安全可控的多智能体系统
  • [进阶篇18] 构建OpenCode事件钩子实现工作流自动化
  • BIOS/UEFI设置全解析:从入门到实战的电脑底层控制指南
  • Python eval安全隐患
  • 茶叶质量分级分类与检测数据集 使用EfficientNet作为基础模型 来训练茶叶质量分级分类与检测数据集模型 茶叶检测及分类数据集的训练
  • Excel RANDBETWEEN函数制作小学数学随机题库:从原理到自动批改
  • AFSim助手再再再再再再升级!
  • IEEE 754浮点数与十六进制转换:原理、代码实现与避坑指南
  • 太空算力网络:分布式计算新范式与AI算力瓶颈的破局思路
  • 「安卓framework基础篇7」从WMS到BufferQueue第一篇 - WMS层级树的初始化过程(基于AOSP13)
  • 定制板多网口怎么做?我从 Switch、NIC、PHY 到 Linux 的一次实战梳理
  • 热力图点亮坚持:ArkUI 日历热力图让鸿蒙打卡页一眼见自律
  • 定义每个科目组配置的合作伙伴角色 和 分配合作伙伴方案给账户组 这两处 感觉做的事情是一样的 为何要开发这些? 有何作用 原理
  • Ollama v0.18.1 深度解析:联网搜索、无头模式与基准测试实战指南
  • 2026玻璃花房铜槽供货商口碑推荐强势出炉,零套路不踩坑,选购看这篇就够 - mypinpai
  • MathType花体字母输入全攻略:从原理到兼容性实战
  • Minecraft Java版与基岩版深度对比:版本选择、模组生态与跨平台联机指南
  • Office Tool Plus:自定义部署与管理Office的终极指南
  • Obsidian LaTeX公式高效输入:插件配置与实战技巧
  • ESP32-S3 MultiNet新增离线语音指令完整教程:Command ID、拼音注册与动作映射
  • 2026降AI率工具红黑榜:降AI率平台怎么选?一文讲透
  • 2026四川乌兹别克斯坦公司注册公证认证,十大出证品牌深度测评,所见即所得不踩雷 - mypinpai
  • Three.js 交互式 3D 人体解剖开源项目
  • Python实战:从零构建钢筋混凝土柱PMM相互作用图计算与可视化工具
  • Science Robotics 封面论文:ZEST零样本具身技能转移技术框架,教会Atlas跳霹雳舞、爬箱子、后空翻
  • 2026四大AI写论文工具深度横评|从降重到润色,各有所长别盲选