
29 ResultSet 结果集遍历与解析从查询结果到实体对象前言图29 ResultSet 结果集遍历与解析从查询结果到实体对象 运行效果截图HarmonyOS NEXT在鸿蒙 RDB 中查询操作返回的不是实体对象而是一个ResultSet——它类似于数据库游标需要手动遍历和解析。将ResultSet转换成 ArkTS 的实体对象是 DAO 层的核心能力之一。本文以鹿鹿·笔迹心理分析项目 5 个 DAO 的parseRow()方法为例深入解析 ResultSet 的遍历策略、getter 方法选择和 null 值处理。鸿蒙官方·ResultSet 文档developer.huawei.com项目源码仓库harmony-app GitHub图ResultSet 从查询到实体对象的完整解析流程false无数据true有数据true有下一行false遍历完毕store.query / querySqlResultSet 游标goToFirstRow?返回空数组parseRow 解析当前行push 到结果数组goToNextRow?rs.close 关闭游标返回实体数组一、ResultSet 基础1.1 数据结构ResultSet是一个游标模型它像指针一样在结果行之间移动┌────┬──────────┬──────────────┬──────┐ │ id │ name │ created_at │ ... │ ← 游标初始在 -1无有效行 ├────┼──────────┼──────────────┤ │ 1 │ 日常随笔 │ 1712345678 │ │ ← goToFirstRow() → 第 0 行 │ 2 │ 心情日记 │ 1712251234 │ │ ← goToNextRow() → 第 1 行 │ 3 │ 灵感碎片 │ 1712167890 │ │ ← goToNextRow() → 第 2 行 └────┴──────────┴──────────────┘ ← goToNextRow() → false1.2 遍历流程constrsawaitstore.query(predicates,COLS)// 方式一单条结果findByIdif(rs.goToFirstRow()){// 判断是否有数据并移动到第一行resultparseRow(rs)}// 方式二多条结果listByArchiveconstresult:Entity[][]while(rs.goToNextRow()){// 遍历所有行result.push(parseRow(rs))}rs.close()// 必须关闭二、getter 方法详解2.1 项目中使用的方法// 数字类型rs.getLong(columnIndex)// 读取 INTEGER 字段id、时间戳、计数rs.getDouble(columnIndex)// 读取 REAL 字段本项目未使用// 字符串类型rs.getString(columnIndex)// 读取 TEXT 字段name、JSON、路径// 列索引获取rs.getColumnIndex(id)// 根据列名获取其在 ResultSet 中的位置2.2 完整五行解析// HandwritingDao.parseRowprivatestaticparseRow(rs:relationalStore.ResultSet):HandwritingEntity{return{id:rs.getLong(rs.getColumnIndex(id)),archive_id:rs.getLong(rs.getColumnIndex(archive_id)),source:rs.getString(rs.getColumnIndex(source)),image_path:rs.getString(rs.getColumnIndex(image_path)),ocr_text:rs.getString(rs.getColumnIndex(ocr_text)),feature_json:rs.getString(rs.getColumnIndex(feature_json)),energy_score:rs.getLong(rs.getColumnIndex(energy_score)),device_type:rs.getString(rs.getColumnIndex(device_type)),word_count:rs.getLong(rs.getColumnIndex(word_count)),write_duration:rs.getLong(rs.getColumnIndex(write_duration)),created_at:rs.getLong(rs.getColumnIndex(created_at))}}2.3 getter 方法对照表列类型推荐 Getter替代 Getter本项目使用INTEGERgetLong()getDouble()可能有精度损失✅ getLongTEXTgetString()无替代✅ getStringREALgetDouble()getLong()截断小数未使用BLOBgetBlob()无替代未使用BIGINTgetLong()getDouble()可能溢出✅ getLong提示项目中所有时间戳created_at都是毫秒级的 13 位整数使用getLong()安全读取。三、遍历模式3.1 单行模式staticasyncfindById(id:number):PromiseHandwritingEntity|null{constpredicatesnewrelationalStore.RdbPredicates(TABLE)predicates.equalTo(id,id)constrsawaitstore.query(predicates,COLS)letresult:HandwritingEntity|nullnullif(rs.goToFirstRow()){// 尝试移动到第 1 行resultHandwritingDao.parseRow(rs)// 解析}rs.close()// 关闭returnresult}关键点goToFirstRow()返回boolean——有数据时返回true并移动到第一行无数据时返回false单条查询结果赋值给let变量需要可修改最终返回null或实体3.2 多行模式staticasynclistByArchive(archiveId:number):PromiseHandwritingEntity[]{constpredicatesnewrelationalStore.RdbPredicates(TABLE)predicates.equalTo(archive_id,archiveId)predicates.orderByDesc(created_at)constrsawaitstore.query(predicates,COLS)constresult:HandwritingEntity[][]while(rs.goToNextRow()){// 从第 0 行开始逐行遍历result.push(HandwritingDao.parseRow(rs))}rs.close()returnresult}流程分析ResultSet 初始状态游标在 -1第一行之前 ↓ 第一次 goToNextRow()游标移动到 0 → 解析第 0 行 第二次 goToNextRow()游标移动到 1 → 解析第 1 行 第三次 goToNextRow()游标移动到 2 → 解析第 2 行 第四次 goToNextRow()游标移动到 3超出行数→ 返回 false → 退出循环3.3 三种移动方法的区别方法初始位置首次调用后无数据时本项目用法goToFirstRow()-1第 0 行返回 false单条结果判断goToNextRow()-1第 0 行返回 false遍历所有行goToLastRow()-1最后一行返回 false未使用goToRow(index)-1指定行越界返回 false未使用重要区别goToFirstRow()和goToNextRow()在首次调用时的行为相同但语义不同——前者判断是否存在数据后者开始遍历。四、关闭 ResultSet4.1 为什么必须 closeconstrsawaitstore.query(predicates,COLS)// ❌ 错误不关闭 ResultSet// ResultSet 持有数据库连接资源不释放// 多次查询后可能导致too many connections错误// ✅ 正确解析完成后立即关闭try{while(rs.goToNextRow()){result.push(parseRow(rs))}}finally{rs.close()}4.2 close 的位置项目中所有 DAO 都在遍历完成后立即调用rs.close()// 单条解析后立即关闭if(rs.goToFirstRow()){resultparseRow(rs)}rs.close()// 在 return 之前关闭// 多条遍历后立即关闭while(rs.goToNextRow()){result.push(parseRow(rs))}rs.close()// 在 return 之前关闭// 聚合查询读取后立即关闭if(rs.goToFirstRow()){cntrs.getLong(rs.getColumnIndex(cnt))}rs.close()// 在 return 之前关闭黄金法则query()和querySql()返回的 ResultSet 一定记得close()。相反insert()、update()、delete()等方法不返回 ResultSet不需要 close。五、各 DAO 的 parseRow 模式汇总5.1 完整对照表DAOparseRow 方法字段数特殊处理UserDao内联在 findByOpenId 中5手动 getLong/getStringHandwritingDao独立的parseRow()11最完整ArchiveDao独立的parseRow()9时间戳、计数ReportDao独立的parseRow()8JSON 字符串RelationDao独立的parseRow()6匹配度、绑定方式5.2 内联 vs 独立 parseRow内联模式UserDaoif(rs.goToFirstRow()){result{id:rs.getLong(rs.getColumnIndex(id)),// ...}}独立模式HandwritingDao// 抽取为私有方法多处复用privatestaticparseRow(rs:relationalStore.ResultSet):HandwritingEntity{// 统一的字段映射}// listByArchive 中使用while(rs.goToNextRow()){result.push(HandwritingDao.parseRow(rs))}// findById 中也使用if(rs.goToFirstRow()){resultHandwritingDao.parseRow(rs)}推荐查询字段多5时使用独立parseRow()字段少≤5且只有一个查询方法时可以考虑内联。六、常见问题和调试6.1 ResultSet 已关闭异常constrsawaitstore.query(predicates,COLS)rs.close()// ❌ 错误关闭后继续访问constnamers.getString(rs.getColumnIndex(name))// → 抛出异常 ResultSet already closed6.2 列名拼写错误// ❌ 错误列名拼写错误rs.getColumnIndex(create_at)// 应改为 created_at// → 返回 -1找不到// → getString(-1) 抛出异常// ✅ 正确与 CREATE TABLE 中的列名完全一致rs.getColumnIndex(created_at)6.3 读取类型错误// ❌ 错误类型不匹配rs.getLong(rs.getColumnIndex(name))// name 是 TEXTgetLong 会返回 0rs.getString(rs.getColumnIndex(id))// id 是 INTEGERgetString 会返回 1// ✅ 正确对应列类型使用对应 getterrs.getString(rs.getColumnIndex(name))// TEXT → getStringrs.getLong(rs.getColumnIndex(id))// INTEGER → getLong七、性能优化7.1 COLS 数组限制列数// 只查询需要的列避免 SELECT *constCOLS:string[][id,archive_id,source,image_path,ocr_text,feature_json,energy_score,device_type,word_count,write_duration,created_at]constrsawaitstore.query(predicates,COLS)// SELECT 列1, 列2, ...7.2 对比查询方式数据传输量解析速度推荐场景query(predicates, [id, name])少快列表页只需部分字段querySql(SELECT * ...)多较慢需要所有字段query(predicates, COLS)适中适中本项目使用总结本文深入解析了鸿蒙 RDB 中 ResultSet 的使用方法游标模型ResultSet 像指针一样在行间移动初始位置在 -1遍历模式第一条用goToFirstRow() 后续用goToNextRow()循环getter 选择INTEGER→getLong()TEXT→getString()parseRow 模式将 ResultSet 当前行转换为 Entity 对象资源管理rs.close()必须在 return 之前调用性能优化COLS 数组限制查询列减少数据传输下一篇文章将介绍原始 SQL 查询与聚合统计——COUNT、GROUP BY 等复杂 SQL 在 DAO 中的应用。如果这篇文章对你有帮助欢迎点赞、收藏⭐、关注你的支持是我持续创作的动力参考资源ResultSet API 参考RdbStore.query 方法[HandwritingDao.parseRow 源码](file:///Users/fiona/Downloads/bijixinli/harmony-app/entry/src/main/ets/features/data/HandwritingDao.ets)[ReportDao.parseRow 源码](file:///Users/fiona/Downloads/bijixinli/harmony-app/entry/src/main/ets/features/data/ReportDao.ets)[ArchiveDao.parseRow 源码](file:///Users/fiona/Downloads/bijixinli/harmony-app/entry/src/main/ets/features/data/ArchiveDao.ets)[RelationDao.parseRow 源码](file:///Users/fiona/Downloads/bijixinli/harmony-app/entry/src/main/ets/features/data/RelationDao.ets)鸿蒙数据持久化指南HarmonyOS 开发文档七、ResultSet 常见陷阱与防御性编程7.1 忘记关闭 ResultSetResultSet 持有数据库连接资源必须在使用完毕后调用rs.close()// ✅ 正确使用 try-finally 确保关闭letrs:relationalStore.ResultSet|nullnulltry{rsawaitstore.query(pred,columns)constresults:User[][]if(rs.goToFirstRow()){do{results.push(parseRow(rs))}while(rs.goToNextRow())}returnresults}finally{rs?.close()// 无论成功还是异常都关闭 ResultSet}7.2 列索引越界获取列值时应先检查列是否存在// ✅ 安全写法先获取列索引constcolIndexrs.getColumnIndex(email)if(colIndex0){constemailrs.getString(colIndex)}// ❌ 危险写法直接用固定索引constemailrs.getString(5)// 如果表结构变化索引可能错位7.3 getter 方法对照表列类型推荐 getter备注TEXTgetString(colIndex)返回 stringINTEGERgetLong(colIndex)返回 numberREALgetDouble(colIndex)返回 numberBLOBgetBlob(colIndex)返回 Uint8ArrayJSON字段getString(colIndex)JSON.parse需要额外解析如果这篇文章对你有帮助欢迎点赞、收藏⭐、关注你的支持是我持续创作的动力