news 2026/4/15 14:42:57

小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/4/14 19:31:23

清华源镜像更新频率对TensorRT使用者的影响

清华源镜像更新频率对TensorRT使用者的影响 在自动驾驶系统实时处理多路摄像头数据的场景中&#xff0c;毫秒级的推理延迟差异可能直接影响决策安全。为了实现这样的性能目标&#xff0c;工程师几乎都会选择 NVIDIA TensorRT —— 这个能将 PyTorch 或 TensorFlow 模型推理速度…

作者头像 李华
网站建设 2026/4/13 17:12:11

Langflow本地部署:环境隔离与快速安装

Langflow本地部署&#xff1a;环境隔离与快速安装 在AI应用开发日益普及的今天&#xff0c;如何快速验证一个基于LangChain的智能体或工作流构想&#xff0c;成了许多开发者面临的实际问题。写一堆样板代码&#xff1f;反复调试依赖版本&#xff1f;这些传统方式不仅耗时&…

作者头像 李华
网站建设 2026/4/13 0:58:53

8 个MBA论文工具推荐,AI降重查重率优化方案

8 个MBA论文工具推荐&#xff0c;AI降重查重率优化方案 论文写作的“三座大山”&#xff1a;时间、重复率与效率 对于MBA学生而言&#xff0c;撰写高质量的论文不仅是学术能力的体现&#xff0c;更是职业发展的关键一步。然而&#xff0c;在实际操作中&#xff0c;许多同学常常…

作者头像 李华
网站建设 2026/4/7 14:33:24

10 个AI论文工具,继续教育学员轻松写完毕业论文!

10 个AI论文工具&#xff0c;继续教育学员轻松写完毕业论文&#xff01; AI 工具助力论文写作&#xff0c;让学术之路更轻松 在继续教育的道路上&#xff0c;撰写毕业论文往往是学员们最头疼的任务之一。面对繁重的写作压力、复杂的格式要求以及严格的查重要求&#xff0c;许多…

作者头像 李华
网站建设 2026/4/13 5:52:28

8 个自考论文降重工具,AI查重率优化推荐

8 个自考论文降重工具&#xff0c;AI查重率优化推荐 论文写作的“重灾区”&#xff1a;时间紧、任务多、降重难 自考学子在完成期末论文时&#xff0c;常常面临一个难以回避的现实——任务繁重、时间紧迫&#xff0c;而论文的质量和重复率又直接影响最终成绩。尤其是在文献综述…

作者头像 李华
网站建设 2026/4/15 6:00:29

如何在 CentOS 上设置 Apache Worker MPM ?

Apache HTTP 服务器是世界上使用最广泛的 web 服务器之一&#xff0c;并可按不同方式配置&#xff0c;以满足各种需求。Apache 多处理模块&#xff08;Multi-Processing Module&#xff0c;MPM&#xff09;是一个管理 Apache 服务器进程的模块。Prefork 和 Worker 是目前最流行…

作者头像 李华