news 2026/8/13 4:55:59

小V健身助手开发手记(五):基于 RDB 的历史记录系统设计与实现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
小V健身助手开发手记(五):基于 RDB 的历史记录系统设计与实现

  • 个人首页: VON

  • 鸿蒙系列专栏: 鸿蒙开发小型案例总结

  • 综合案例 :鸿蒙综合案例开发

  • 鸿蒙6.0:从0开始的开源鸿蒙6.0.0

  • 鸿蒙5.0:鸿蒙5.0零基础入门到项目实战

  • Electron适配开源鸿蒙专栏:Electron for OpenHarmony

  • Flutter 适配开源鸿蒙专栏:Flutter for OpenHarmony

  • 本文所属专栏:鸿蒙综合案例开发

  • 本文atomgit地址:小V健身

小V健身助手开发手记(五)

  • 数据库架构设计与实现详解
    • 一、为什么选择关系型数据库?
    • 二、数据模型设计:从业务对象到数据库表
      • 1. 业务对象:`RecordPO`
      • 2. 数据库表结构
    • 三、列映射机制:ColumnInfo 与类型安全
    • 四、通用数据记录容器:DataRecord
    • 五、数据库工具类:DBUtil(单例 + 封装)
      • 1. 初始化数据库
      • 2. 建表
      • 3. CRUD 操作封装
        • 插入(Insert)
        • 查询(Query)
    • 六、业务模型层:RecordModel
    • 七、辅助模块:运动项管理与首选项
    • 八、潜在问题与改进建议
    • 九 功能展示
    • 十、总结

数据库架构设计与实现详解

目前项目已经移植到坚果派,可以通过坚果派直接访问:项目传送门

在小V健身助手的开发过程中,数据持久化是支撑整个应用功能运转的核心模块。无论是用户每日打卡记录、运动项目管理,还是历史数据统计分析,都离不开一套稳定、高效、可维护的本地数据库系统。本文将深入剖析我们在小V健身助手中采用的数据库设计方案,从表结构定义、ORM映射、CRUD操作封装,到数据库初始化流程和工具类抽象,全面解读其技术实现细节。


一、为什么选择关系型数据库?

小V健身助手运行在HarmonyOS平台,基于ArkTS语言开发。HarmonyOS提供了多种本地存储方案,包括:

  • Preferences:轻量级键值对存储,适合配置项;
  • Relational Database(RDB):SQLite兼容的关系型数据库,适合结构化数据;
  • Distributed Data Object(DDM):用于跨设备同步。

考虑到我们的核心数据——运动记录(如跳绳次数、跑步时长等)具有明确的字段结构、需要支持复杂查询(如“按日期范围查询”)、且可能随时间增长形成大量记录,我们最终选择了Relational Database(RDB)作为主存储引擎。

优势

  • 支持事务、索引、约束;
  • 可高效执行条件查询、分组、排序;
  • ArkTS 提供了完善的relationalStoreAPI 封装。

二、数据模型设计:从业务对象到数据库表

1. 业务对象:RecordPO

首先,我们定义了代表一条运动记录的业务对象RecordPO(Persistent Object):

exportdefaultclassRecordPO{id?:number;// 主键,自增keepId:number=0;// 关联的运动项IDamount:number=0;// 计划完成量(如:跳绳1000次)createTime?:number;// 记录创建时间(时间戳)successAmount:number=0;// 实际完成量}

该类完全对应一条数据库记录,字段命名采用驼峰式(符合TS规范),而数据库列名则使用下划线风格(符合SQL惯例)。

2. 数据库表结构

根据RecordPO,我们设计了如下建表语句:

constCREATE_TABLE_SQL:string=`( id INTEGER PRIMARY KEY AUTOINCREMENT, keep_id INTEGER NOT NULL, amount INTEGER NOT NULL, create_time INTEGER NOT NULL, success_amount INTEGER NOT NULL )`;
  • 表名为recode(注:此处应为record,属笔误,但不影响功能);
  • 所有字段均为NOT NULL,确保数据完整性;
  • id为主键并自动递增;
  • 时间以毫秒时间戳(INTEGER)存储,便于跨平台处理。

🔍注意:虽然amountRecordPO中是number类型,但在数据库中我们统一用INTEGER存储。若未来需支持小数(如公里数),可改为REAL


三、列映射机制:ColumnInfo 与类型安全

为了在业务对象与数据库列之间建立可靠映射,我们引入了ColumnInfo接口和ColumnType枚举:

exportinterfaceColumnInfo{name:string;// RecordPO 中的属性名(如 'keepId')columnName:string;// 数据库列名(如 'keep_id')type:ColumnType;// 列的数据类型}exportenumColumnType{LONG,DOUBLE,STRING,BLOB}

并定义了具体的列映射数组:

constCOLUMNS:ColumnInfo[]=[{name:'id',columnName:'id',type:ColumnType.LONG},{name:'keepId',columnName:'keep_id',type:ColumnType.LONG},{name:'amount',columnName:'amount',type:ColumnType.DOUBLE},// 注意:此处设为DOUBLE,但建表用INTEGER,存在不一致{name:'createTime',columnName:'create_time',type:ColumnType.LONG},{name:'successAmount',columnName:'success_amount',type:ColumnType.LONG}];

⚠️潜在问题amountCOLUMNS中被标记为DOUBLE,但建表 SQL 使用INTEGER。这可能导致读取时类型转换异常。建议统一为LONG或根据实际需求调整。

此设计实现了解耦:业务层无需关心数据库列名,只需通过ColumnInfo配置即可完成自动映射。


四、通用数据记录容器:DataRecord

由于 ArkTS 的ValuesBucket要求键为字符串、值为基本类型,我们封装了一个通用的DataRecord类:

exportclassDataRecord{privatedata:Map<string,number|string|boolean|Uint8Array|null|undefined>=newMap();setValue(key:string,value:any):void{this.data.set(key,value);}getValue(key:string):any{returnthis.data.get(key);}hasValue(key:string):boolean{constvalue=this.data.get(key);returntypeofvalue!=='undefined'&&value!==null;}}

它作为中间层,将RecordPO转换为可被数据库操作的格式,避免直接暴露底层ValuesBucket


五、数据库工具类:DBUtil(单例 + 封装)

DBUtil是整个数据库操作的核心枢纽,采用单例模式确保全局唯一实例:

classDBUtil{privaterdbStore!:relationalStore.RdbStore;privatestaticinstance:DBUtil|null=null;privateconstructor(){}staticgetInstance():DBUtil{if(!DBUtil.instance){DBUtil.instance=newDBUtil();}returnDBUtil.instance;}}

1. 初始化数据库

通过initDB方法,在应用启动时绑定上下文并打开数据库:

initDB(context:common.UIAbilityContext):Promise<void>{constconfig:relationalStore.StoreConfig={name:'Small_V_Health.db',securityLevel:1,// 安全等级};returnrelationalStore.getRdbStore(context,config).then(rdbStore=>{this.rdbStore=rdbStore;});}

2. 建表

提供通用的createTable方法:

createTable(createSQL:string):Promise<void>{returnthis.rdbStore.executeSql(`CREATE TABLE IF NOT EXISTS recode${createSQL}`);}

💡 建议:表名应作为参数传入,或从常量读取,避免硬编码。

3. CRUD 操作封装

插入(Insert)
insert(tableName:string,obj:DataRecord,columns:ColumnInfo[]):Promise<number>{constvalue=this.buildValueBucket(obj,columns);returnnewPromise((resolve,reject)=>{this.rdbStore.insert(tableName,value,(err,id)=>{err?reject(err):resolve(id);});});}

其中buildValueBucket负责将DataRecord转为ValuesBucket

buildValueBucket(obj:DataRecord,columns:ColumnInfo[]):relationalStore.ValuesBucket{constvalue:relationalStore.ValuesBucket={};columns.forEach(info=>{if(obj.hasValue(info.name)){value[info.columnName]=obj.getValue(info.name);}});returnvalue;}
查询(Query)

查询是最复杂的部分,需将ResultSet转回DataRecord数组:

queryForList(predicates:RdbPredicates,columns:ColumnInfo[]):Promise<DataRecord[]>{returnnewPromise((resolve,reject)=>{this.rdbStore.query(predicates,columns.map(c=>c.columnName),(err,result)=>{if(err)reject(err);else{try{constrecords=this.parseResultSet(result,columns);resolve(records);}finally{result.close();// 必须关闭结果集,防止内存泄漏}}});});}

parseResultSet方法逐行解析:

parseResultSet(result:ResultSet,columns:ColumnInfo[]):DataRecord[]{constarr:DataRecord[]=[];if(result.rowCount<=0)returnarr;result.goToFirstRow();while(!result.isAtLastRow){constrecord=this.extractRow(result,columns);arr.push(record);result.goToNextRow();}// 处理最后一行(API设计缺陷:isAtLastRow 不包含最后一行)if(result.rowCount>0){constrecord=this.extractRow(result,columns);arr.push(record);}returnarr;}privateextractRow(result:ResultSet,columns:ColumnInfo[]):DataRecord{constrecord=newDataRecord();columns.forEach(info=>{constidx=result.getColumnIndex(info.columnName);letval:any;switch(info.type){caseColumnType.LONG:val=result.getLong(idx);break;caseColumnType.DOUBLE:val=result.getDouble(idx);break;caseColumnType.STRING:val=result.getString(idx);break;caseColumnType.BLOB:val=result.getBlob(idx);break;default:val=null;}record.setValue(info.name,val);});returnrecord;}

📌关键点:HarmonyOS 的ResultSet遍历逻辑较为特殊,需手动处理“最后一行”,这是官方 API 的一个常见陷阱。


六、业务模型层:RecordModel

DBUtil之上,我们构建了RecordModel,提供面向业务的接口:

classRecordModel{// 表常量privatereadonlyTABLE_NAME='recode';privatereadonlyID_COLUMN='id';privatereadonlyDATE_COLUMN='create_time';insert(record:RecordPO):Promise<number>{constdataRecord=newDataRecord();dataRecord.setValue('id',record.id);dataRecord.setValue('keepId',record.keepId);// ... 其他字段returnDBUtil.insert(this.TABLE_NAME,dataRecord,COLUMNS);}asyncqueryByDate(date:number):Promise<RecordPO[]>{constpredicates=newRdbPredicates(this.TABLE_NAME);conststartOfDay=date;constendOfDay=date+24*60*60*1000-1;predicates.between(this.DATE_COLUMN,startOfDay,endOfDay);constdataRecords=awaitDBUtil.queryForList(predicates,COLUMNS);returndataRecords.map(dr=>{constpo=newRecordPO();po.id=dr.getValue('id')asnumber;po.keepId=dr.getValue('keepId')asnumber;// ... 赋值其他字段returnpo;});}// delete / update 略...}

这种分层设计使得:

  • 业务层只与RecordPORecordModel交互;
  • 数据访问层(DBUtil)完全屏蔽了 SQL 和底层 API 细节;
  • 可测试性强:可 mockRecordModel返回模拟数据。

七、辅助模块:运动项管理与首选项

除了核心记录表,我们还维护了一个静态的运动项列表:

constkeeps:RecordItem[]=[newRecordItem(0,'跳绳',$r('app.media.home_ic_swimming'),'/小时',600),// ...];

并通过ItemModel提供查询:

classItemModel{getById(id:number){returnkeeps[id];}list(){returnkeeps;}}

🔄优化建议:未来可将keeps存入数据库,支持用户自定义运动项目。

同时,使用PreferenceUtil管理用户设置(如首次启动状态、目标提醒等),其基于@ohos.data.preferences实现,采用单例+异步加载模式,确保线程安全。


八、潜在问题与改进建议

尽管当前架构已满足 MVP 需求,但仍存在可优化空间:

问题建议
表名recode拼写错误改为record,并添加数据库版本迁移逻辑
amount类型不一致(INTEGER vs DOUBLE)统一为REAL或明确业务含义
RecordPO字段未校验非空添加构造函数或 Builder 模式
查询遍历逻辑冗余封装通用ResultSettoT[]工具
缺少索引create_timekeep_id添加索引提升查询性能
无事务支持在批量插入/更新时启用事务

九 功能展示

十、总结

小V健身助手的数据库模块通过分层架构 + 类型安全映射 + 单例工具封装,实现了高内聚、低耦合的设计目标。从RecordPODataRecord,再到ValuesBucketResultSet,每一步转换都经过精心抽象,既保证了代码可读性,又提升了可维护性。

这套方案不仅适用于健身记录场景,也可作为 HarmonyOS 应用本地数据库开发的参考模板。未来,我们将引入数据库版本管理、加密存储、以及云端同步能力,进一步提升数据安全与用户体验。

代码即文档,架构即承诺。在小V健身助手的演进之路上,稳健的数据基石,是我们交付可靠体验的底气所在。


附:关键常量与类型定义汇总

// 表名与列名constTABLE_NAME='recode';constID_COLUMN='id';constDATE_COLUMN='create_time';// 列信息constCOLUMNS:ColumnInfo[]=[{name:'id',columnName:'id',type:ColumnType.LONG},{name:'keepId',columnName:'keep_id',type:ColumnType.LONG},{name:'amount',columnName:'amount',type:ColumnType.DOUBLE},{name:'createTime',columnName:'create_time',type:ColumnType.LONG},{name:'successAmount',columnName:'success_amount',type:ColumnType.LONG}];// 建表语句constCREATE_TABLE_SQL=`( ... )`;

通过以上设计,小V健身助手得以在用户每一次点击“完成”按钮时,默默而可靠地将汗水转化为数据,为健康生活留下数字足迹。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/12 12:40:10

【Dify高性能视频处理指南】:精准帧率设置提升提取速度300%

第一章&#xff1a;Dify视频帧提取的核心机制Dify平台在处理视频内容理解时&#xff0c;依赖其高效的视频帧提取机制来实现对视觉信息的结构化解析。该机制通过精准的时间戳控制与自适应采样策略&#xff0c;确保关键帧被有效捕获&#xff0c;同时避免冗余数据的生成。帧提取流…

作者头像 李华
网站建设 2026/8/12 10:35:45

为什么你的Tesseract在Dify中处理慢?这5个批量优化关键点必须掌握

第一章&#xff1a;Dify Tesseract 的批量处理在自动化文档识别与数据提取场景中&#xff0c;Dify 集成 Tesseract OCR 实现高效的批量图像文本识别&#xff0c;显著提升处理效率。通过脚本化调度与配置优化&#xff0c;可对成百上千张图像文件进行并行识别&#xff0c;适用于发…

作者头像 李华
网站建设 2026/8/12 10:35:33

CDM(充电器件模型)导致芯片失效原因

CDM&#xff08;Charged-Device Model&#xff0c;充电器件模型&#xff09;导致的芯片失效&#xff0c;核心机理是“芯片自身带电→某一引脚瞬间接地→内部电荷在纳秒级时间内形成极高峰值电流→敏感结构被击穿”。常见失效原因可归纳为三大类&#xff1a;介质击穿&#xff08…

作者头像 李华
网站建设 2026/8/12 11:43:05

IL-2:调控免疫稳态的“双面因子”

在免疫系统的复杂调控网络中&#xff0c;白细胞介素-2&#xff08;IL-2&#xff09;无疑是核心枢纽之一。自1976年被发现并命名为“T细胞生长因子”以来&#xff0c;IL-2凭借其既能驱动免疫攻击、又能维持免疫耐受的“双面性”&#xff0c;成为连接基础免疫学与临床治疗的关键分…

作者头像 李华
网站建设 2026/8/12 10:35:33

【环境风险评估效能革命】:基于R语言的动态监测系统搭建实录

第一章&#xff1a;环境风险评估的范式转型与R语言机遇传统环境风险评估长期依赖静态模型和经验公式&#xff0c;难以应对复杂生态系统中的非线性动态与不确定性。随着大数据与开源计算生态的发展&#xff0c;评估范式正从“假设驱动”向“数据驱动”转型。R语言凭借其强大的统…

作者头像 李华
网站建设 2026/8/11 16:22:50

揭秘Dify中PDF加密与权限验证机制:企业级数据防护必备技能

第一章&#xff1a;揭秘Dify中PDF加密与权限验证机制&#xff1a;企业级数据防护必备技能在企业级应用中&#xff0c;敏感文档的安全分发至关重要。Dify 通过集成 PDF 加密与细粒度权限验证机制&#xff0c;确保生成的 PDF 文件仅能被授权用户访问和操作。该机制结合 AES-256 加…

作者头像 李华