鸿蒙应用开发实战【04】— 关系型数据库设计与封装(relationalStore)

鸿蒙应用开发实战【04】— 关系型数据库设计与封装(relationalStore)
鸿蒙应用开发实战【04】— 关系型数据库设计与封装relationalStore前言欢迎加入开源鸿蒙跨平台社区https://openharmonycrossplatform.csdn.net号码助手的核心价值在于持久化存储用户的卡号与应用绑定关系。HarmonyOS NEXT 提供了ohos.data.relationalStore即基于 SQLite 的本地关系型数据库。本篇从数据库初始化、表结构设计到 DAO 层封装完整讲解号码助手数据层的实现思路。本篇涵盖relationalStore API、DatabaseService 初始化、CardEntity / AppBindingEntity 数据模型、CardDao / AppBindingDao 的 CRUD 实现、parseRow 解析模式、错误处理规范。一、relationalStore 概述1.1 HarmonyOS 数据持久化方案对比方案适用场景特点relationalStore结构化数据、多表关联SQL 支持完整适合复杂查询preferences轻量 KV 配置仅支持简单键值对distributedKVStore跨设备同步适合多端数据共享场景fileIO文件读写适合大文件存储号码助手选择relationalStore的原因需要多表关联卡号 ↔ 应用绑定需要聚合查询按状态统计数量需要批量操作批量更新/删除需要数据导出序列化为 JSON1.2 relationalStore 核心 APIimport{relationalStore}fromohos.data.relationalStore// 主要类relationalStore.getRdbStore()// 获取/创建数据库实例relationalStore.RdbPredicates// 查询条件构造器relationalStore.RdbStore// 数据库操作接口// 主要操作rdb.executeSql()// 执行 DDL/DML SQL 语句rdb.insert()// 插入记录rdb.update()// 更新记录rdb.delete()// 删除记录rdb.query()// 查询记录集rdb.batchInsert()// 批量插入二、DatabaseService — 数据库初始化2.1 数据库配置// DatabaseService.etsimport{relationalStore}fromohos.data.relationalStoreimport{common}fromkit.AbilityKit// 数据库配置constDB_CONFIG:relationalStore.StoreConfig{name:number_assistant.db,// 数据库文件名securityLevel:relationalStore.SecurityLevel.S2// 安全级别 S2加密存储}安全级别说明级别含义适用场景S1低安全公开数据S2中安全用户个人数据本项目使用S3高安全金融/医疗数据S4极高安全国家安全级别2.2 建表 SQL// 建表语句常量单独抽出方便版本升级时修改constCREATE_CARDS_TABLECREATE TABLE IF NOT EXISTS cards ( id INTEGER PRIMARY KEY AUTOINCREMENT, label TEXT NOT NULL, phone TEXT NOT NULL, carrier TEXT NOT NULL DEFAULT , color TEXT NOT NULL DEFAULT blue, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL )constCREATE_APP_BINDINGS_TABLECREATE TABLE IF NOT EXISTS app_bindings ( id INTEGER PRIMARY KEY AUTOINCREMENT, app_name TEXT NOT NULL, icon_key TEXT NOT NULL DEFAULT #4F7CFF, category TEXT NOT NULL DEFAULT APP, card_id INTEGER NOT NULL, status TEXT NOT NULL DEFAULT 使用中, remark TEXT NOT NULL DEFAULT , source TEXT NOT NULL DEFAULT 手动, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL, FOREIGN KEY (card_id) REFERENCES cards(id) )constCREATE_SMS_CANDIDATES_TABLECREATE TABLE IF NOT EXISTS sms_candidates ( id INTEGER PRIMARY KEY AUTOINCREMENT, app_name TEXT NOT NULL, source_sms TEXT NOT NULL DEFAULT , status TEXT NOT NULL DEFAULT pending, created_at INTEGER NOT NULL )2.3 初始化实现// DatabaseService.ets 完整实现exportclassDatabaseService{privatestaticrdbStore:relationalStore.RdbStore|nullnull/** * 初始化数据库由 EntryAbility.onWindowStageCreate 调用 * 异步执行不阻塞 UI 启动 */staticasyncinit(context:common.UIAbilityContext):Promisevoid{if(DatabaseService.rdbStore!null){return// 已初始化幂等处理}try{conststoreawaitrelationalStore.getRdbStore(context,DB_CONFIG)// 执行建表语句IF NOT EXISTS 保证幂等awaitstore.executeSql(CREATE_CARDS_TABLE)awaitstore.executeSql(CREATE_APP_BINDINGS_TABLE)awaitstore.executeSql(CREATE_SMS_CANDIDATES_TABLE)DatabaseService.rdbStorestoreconsole.info([DatabaseService] 数据库初始化成功)}catch(e){console.error([DatabaseService] 初始化失败:,JSON.stringify(e))throwe}}/** 获取数据库实例供各 DAO 调用 */staticgetStore():relationalStore.RdbStore{if(!DatabaseService.rdbStore){thrownewError(数据库尚未初始化请确保 DatabaseService.init() 已被调用)}returnDatabaseService.rdbStore}}时序注意DatabaseService.init()在EntryAbility.onWindowStageCreate中异步调用页面加载和 DB 初始化并行进行。所以页面首次onPageShow时 DB 可能尚未就绪DAO 调用必须有try/catch。三、数据模型设计3.1 E-R 图图1号码助手数据库 E-R 图 — cards 与 app_bindings 是 1:N 关系cards (1) ──────── (N) app_bindings id ◄──────────────── card_id label app_name phone status color category3.2 CardEntity 数据模型// Models.ets// 卡号颜色对应 UI 中的色块选择exporttypeCardColorblue|purple|green|orange|pinkexportinterfaceCardEntity{id?:number// 自增主键新建时为 undefinedlabel:string// 用户备注如主卡、工作卡phone:string// 完整手机号本地存储不上传carrier:string// 运营商中国移动 / 中国联通 / 中国电信color:CardColor// 卡片颜色主题created_at:number// 创建时间Unix 毫秒时间戳updated_at:number// 最后更新时间}3.3 AppBindingEntity 数据模型// 应用绑定状态状态机流转规则使用中 → 待换绑/待注销 → 已停用exporttypeBindingStatus使用中|待换绑|待注销|已停用// 应用分类来自设计稿的三类exporttypeAppCategoryAPP|网站|小程序exportinterfaceAppBindingEntity{id?:numberapp_name:string// 应用名称如微信、支付宝icon_key:string// 图标颜色 hex用颜色代替图标减少存储category:AppCategory// 应用分类card_id:number// 关联的卡号 ID外键status:BindingStatus// 当前绑定状态remark:string// 备注信息source:string// 数据来源手动 / 粘贴导入 / 短信导入created_at:numberupdated_at:number}四、CardDao 完整实现4.1 parseRow — 结果集解析模式// CardDao.etsimport{relationalStore}fromohos.data.relationalStoreimport{DatabaseService}from./DatabaseServiceimport{CardEntity,CardColor}from./ModelsexportclassCardDao{/** * 将结果集当前行解析为 CardEntity * 所有 DAO 都使用此模式先 moveToFirstRow()然后 parseRow() */privatestaticparseRow(rs:relationalStore.ResultSet):CardEntity{return{id:rs.getLong(rs.getColumnIndex(id)),label:rs.getString(rs.getColumnIndex(label)),phone:rs.getString(rs.getColumnIndex(phone)),carrier:rs.getString(rs.getColumnIndex(carrier)),color:rs.getString(rs.getColumnIndex(color))asCardColor,created_at:rs.getLong(rs.getColumnIndex(created_at)),updated_at:rs.getLong(rs.getColumnIndex(updated_at))}}4.2 listAll — 查询所有卡号/** 查询所有卡号按创建时间倒序 */staticasynclistAll():PromiseCardEntity[]{constrdbDatabaseService.getStore()constpredicatesnewrelationalStore.RdbPredicates(cards)predicates.orderByDesc(created_at)constrsawaitrdb.query(predicates)constresults:CardEntity[][]if(rs.goToFirstRow()){do{results.push(CardDao.parseRow(rs))}while(rs.goToNextRow())}rs.close()// 务必关闭结果集释放内存returnresults}4.3 create — 插入新卡号/** 插入新卡号返回新记录的 ID */staticasynccreate(card:CardEntity):Promisenumber{constrdbDatabaseService.getStore()// ValuesBucket要插入的键值对constvalues:relationalStore.ValuesBucket{label:card.label,phone:card.phone,carrier:card.carrier,color:card.color,created_at:card.created_at,updated_at:card.updated_at}constrowIdawaitrdb.insert(cards,values)if(rowId0){thrownewError(CardDao.create: 插入失败rowId 0)}returnrowId}4.4 update — 更新卡号/** 更新卡号信息 */staticasyncupdate(card:CardEntity):Promisevoid{if(!card.id){thrownewError(CardDao.update: card.id 不能为空)}constrdbDatabaseService.getStore()constpredicatesnewrelationalStore.RdbPredicates(cards)predicates.equalTo(id,card.id)constvalues:relationalStore.ValuesBucket{label:card.label,phone:card.phone,carrier:card.carrier,color:card.color,updated_at:Date.now()}constrowsAffectedawaitrdb.update(values,predicates)if(rowsAffected!1){thrownewError(CardDao.update: 期望更新 1 行实际${rowsAffected}行)}}4.5 deleteById — 删除卡号/** 删除指定 ID 的卡号 */staticasyncdeleteById(id:number):Promisevoid{constrdbDatabaseService.getStore()constpredicatesnewrelationalStore.RdbPredicates(cards)predicates.equalTo(id,id)awaitrdb.delete(predicates)}}五、AppBindingDao 高级查询5.1 countByStatus — 按状态统计// 返回类型接口必须声明ArkTS 严格要求exportinterfaceStatusCount{status:stringcount:number}exportclassAppBindingDao{/** * 按状态统计绑定数量 * 返回类似[{ status: 待换绑, count: 3 }, ...] */staticasynccountByStatus():PromiseStatusCount[]{constrdbDatabaseService.getStore()constsqlSELECT status, COUNT(*) as count FROM app_bindings GROUP BY statusconstrsawaitrdb.querySql(sql)constresults:StatusCount[][]if(rs.goToFirstRow()){do{constitem:StatusCount{status:rs.getString(rs.getColumnIndex(status)),count:rs.getLong(rs.getColumnIndex(count))}results.push(item)}while(rs.goToNextRow())}rs.close()returnresults}5.2 batchUpdateStatus — 批量状态变更/** * 批量更新多个绑定的状态 * param ids 要更新的记录 ID 列表 * param status 新状态 * param updatedAt 更新时间戳 */staticasyncbatchUpdateStatus(ids:number[],status:BindingStatus,updatedAt:number):Promisevoid{if(ids.length0)returnconstrdbDatabaseService.getStore()constpredicatesnewrelationalStore.RdbPredicates(app_bindings)predicates.in(id,idsasrelationalStore.ValueType[])constvalues:relationalStore.ValuesBucket{status:status,updated_at:updatedAt}awaitrdb.update(values,predicates)}5.3 findById — 精确查询单条/** 根据 ID 查询单条绑定记录不存在返回 null */staticasyncfindById(id:number):PromiseAppBindingEntity|null{constrdbDatabaseService.getStore()constpredicatesnewrelationalStore.RdbPredicates(app_bindings)predicates.equalTo(id,id)constrsawaitrdb.query(predicates)if(rs.getRowCount()0){rs.close()returnnull}rs.goToFirstRow()constentityAppBindingDao.parseRow(rs)rs.close()returnentity}}六、DAO 错误处理规范6.1 页面调用 DAO 的标准模式// AppDetailPage.ets 中加载数据的标准写法Stateloading:booleantrueStateloadError:booleanfalseStatebinding:AppBindingEntity|nullnullprivateasyncloadData():Promisevoid{if(this.bindingId0){promptAction.showToast({message:参数错误无法加载详情})this.loadingfalsethis.loadErrortruereturn}try{constbawaitAppBindingDao.findById(this.bindingId)if(!b){promptAction.showToast({message:该绑定已被删除})this.getUIContext().getRouter().back()return}this.bindingb}catch(e){console.error([AppDetailPage] loadData 失败:,JSON.stringify(e))promptAction.showToast({message:加载失败请重试})this.loadErrortrue// 展示错误 UI}finally{this.loadingfalse// 无论成功失败关闭加载态}}6.2 三态 UI 对应build(){Column(){if(this.loading){// ① 加载中LoadingProgress().width(40).height(40).color(AppColors.PRIMARY)}elseif(this.loadError||!this.binding){// ② 加载失败Column(){Text(加载失败请返回重试).fontColor(AppColors.TEXT_2)Button(返回).onClick(()router.back())}}else{// ③ 正常内容this.DetailContent(this.binding)}}}七、数据库性能优化要点7.1 关闭结果集// ❌ 忘记 close()导致内存泄漏constrsawaitrdb.query(predicates)rs.goToFirstRow()constdataparseRow(rs)// 没有 rs.close()// ✅ 每次查询后必须 close()constrsawaitrdb.query(predicates)try{// ... 处理结果}finally{rs.close()// 确保在任何情况下都关闭}7.2 避免全表扫描// ❌ 查询所有数据再在内存中过滤constallawaitAppBindingDao.listAll()constbyCardall.filter(bb.card_idcardId)// ✅ 在数据库层过滤staticasynclistByCardId(cardId:number):PromiseAppBindingEntity[]{constpredicatesnewrelationalStore.RdbPredicates(app_bindings)predicates.equalTo(card_id,cardId)// 数据库层过滤// ...}八、本篇小结本篇完成了号码助手数据层的全面解析DatabaseService统一初始化入口S2 安全级别幂等设计数据模型CardEntity、AppBindingEntity、SmsCandidateEntity三张表DAO 层parseRow解析模式、CRUD 方法、批量操作、聚合查询错误处理try/finally 关闭结果集try/catch 统一返回 toast loadError下一篇将进入首页HomePage的完整开发介绍如何将 DAO 层数据驱动 UI 渲染、实现加载状态管理和筛选功能。九、参考资料本系列相关文章鸿蒙应用开发实战【27】— DatabaseService 数据库初始化详解鸿蒙应用开发实战【31】— CardDao CRUD 完整实现鸿蒙应用开发实战【37】— 数据统计countByStatus 聚合查询官方文档relationalStore 概述relationalStore API 参考RdbPredicates 使用说明数据库安全等级如果这篇文章对你有帮助欢迎点赞、收藏⭐、关注你的支持是我持续创作的动力相关资源开源鸿蒙跨平台社区https://openharmonycrossplatform.csdn.netrelationalStore 文档https://developer.huawei.com/consumer/cn/doc/harmonyos-references/js-apis-data-relationalstoreHarmonyOS 数据管理指南https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/data-management-overview