29 ResultSet 结果集遍历与解析:从查询结果到实体对象
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 从查询到实体对象的完整解析流程
一、ResultSet 基础
1.1 数据结构
ResultSet 是一个游标模型,它像指针一样在结果行之间移动:
┌────┬──────────┬──────────────┬──────┐
│ id │ name │ created_at │ ... │ ← 游标初始在 -1(无有效行)
├────┼──────────┼──────────────┤
│ 1 │ 日常随笔 │ 1712345678 │ │ ← goToFirstRow() → 第 0 行
│ 2 │ 心情日记 │ 1712251234 │ │ ← goToNextRow() → 第 1 行
│ 3 │ 灵感碎片 │ 1712167890 │ │ ← goToNextRow() → 第 2 行
└────┴──────────┴──────────────┘ ← goToNextRow() → false
1.2 遍历流程
const rs = await store.query(predicates, COLS)
// 方式一:单条结果(findById)
if (rs.goToFirstRow()) { // 判断是否有数据,并移动到第一行
result = parseRow(rs)
}
// 方式二:多条结果(listByArchive)
const result: 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.parseRow
private static parseRow(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 | 本项目使用 |
|---|---|---|---|
| INTEGER | getLong() |
getDouble()(可能有精度损失) |
✅ getLong |
| TEXT | getString() |
无替代 | ✅ getString |
| REAL | getDouble() |
getLong()(截断小数) |
未使用 |
| BLOB | getBlob() |
无替代 | 未使用 |
| BIGINT | getLong() |
getDouble() 可能溢出 |
✅ getLong |
提示:项目中所有时间戳(
created_at)都是毫秒级的 13 位整数,使用getLong()安全读取。
三、遍历模式
3.1 单行模式
static async findById(id: number): Promise<HandwritingEntity | null> {
const predicates = new relationalStore.RdbPredicates(TABLE)
predicates.equalTo('id', id)
const rs = await store.query(predicates, COLS)
let result: HandwritingEntity | null = null
if (rs.goToFirstRow()) { // 尝试移动到第 1 行
result = HandwritingDao.parseRow(rs) // 解析
}
rs.close() // 关闭
return result
}
关键点:
goToFirstRow()返回boolean——有数据时返回true并移动到第一行,无数据时返回false- 单条查询结果赋值给
let变量(需要可修改),最终返回null或实体
3.2 多行模式
static async listByArchive(archiveId: number): Promise<HandwritingEntity[]> {
const predicates = new relationalStore.RdbPredicates(TABLE)
predicates.equalTo('archive_id', archiveId)
predicates.orderByDesc('created_at')
const rs = await store.query(predicates, COLS)
const result: HandwritingEntity[] = []
while (rs.goToNextRow()) { // 从第 0 行开始逐行遍历
result.push(HandwritingDao.parseRow(rs))
}
rs.close()
return result
}
流程分析:
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() 在首次调用时的行为相同,但语义不同——前者"判断是否存在数据",后者"开始遍历"。
四、关闭 ResultSet
4.1 为什么必须 close
const rs = await store.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()) {
result = parseRow(rs)
}
rs.close() // 在 return 之前关闭
// 多条:遍历后立即关闭
while (rs.goToNextRow()) {
result.push(parseRow(rs))
}
rs.close() // 在 return 之前关闭
// 聚合查询:读取后立即关闭
if (rs.goToFirstRow()) {
cnt = rs.getLong(rs.getColumnIndex('cnt'))
}
rs.close() // 在 return 之前关闭
黄金法则:
query()和querySql()返回的 ResultSet 一定记得close()。相反,insert()、update()、delete()等方法不返回 ResultSet,不需要 close。
五、各 DAO 的 parseRow 模式汇总
5.1 完整对照表
| DAO | parseRow 方法 | 字段数 | 特殊处理 |
|---|---|---|---|
| UserDao | 内联在 findByOpenId 中 | 5 | 手动 getLong/getString |
| HandwritingDao | 独立的 parseRow() |
11 | 最完整 |
| ArchiveDao | 独立的 parseRow() |
9 | 时间戳、计数 |
| ReportDao | 独立的 parseRow() |
8 | JSON 字符串 |
| RelationDao | 独立的 parseRow() |
6 | 匹配度、绑定方式 |
5.2 内联 vs 独立 parseRow
内联模式(UserDao):
if (rs.goToFirstRow()) {
result = {
id: rs.getLong(rs.getColumnIndex('id')),
// ...
}
}
独立模式(HandwritingDao):
// 抽取为私有方法,多处复用
private static parseRow(rs: relationalStore.ResultSet): HandwritingEntity {
// 统一的字段映射
}
// listByArchive 中使用
while (rs.goToNextRow()) {
result.push(HandwritingDao.parseRow(rs))
}
// findById 中也使用
if (rs.goToFirstRow()) {
result = HandwritingDao.parseRow(rs)
}
推荐:查询字段多(>5)时使用独立 parseRow();字段少(≤5)且只有一个查询方法时可以考虑内联。
六、常见问题和调试
6.1 ResultSet 已关闭异常
const rs = await store.query(predicates, COLS)
rs.close()
// ❌ 错误:关闭后继续访问
const name = rs.getString(rs.getColumnIndex('name'))
// → 抛出异常 "ResultSet already closed"
6.2 列名拼写错误
// ❌ 错误:列名拼写错误
rs.getColumnIndex('create_at') // 应改为 created_at
// → 返回 -1(找不到)
// → getString(-1) 抛出异常
// ✅ 正确:与 CREATE TABLE 中的列名完全一致
rs.getColumnIndex('created_at')
6.3 读取类型错误
// ❌ 错误:类型不匹配
rs.getLong(rs.getColumnIndex('name')) // name 是 TEXT,getLong 会返回 0
rs.getString(rs.getColumnIndex('id')) // id 是 INTEGER,getString 会返回 "1"
// ✅ 正确:对应列类型使用对应 getter
rs.getString(rs.getColumnIndex('name')) // TEXT → getString
rs.getLong(rs.getColumnIndex('id')) // INTEGER → getLong
七、性能优化
7.1 COLS 数组限制列数
// 只查询需要的列,避免 SELECT *
const COLS: string[] = [
'id', 'archive_id', 'source', 'image_path', 'ocr_text',
'feature_json', 'energy_score', 'device_type',
'word_count', 'write_duration', 'created_at'
]
const rs = await store.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 忘记关闭 ResultSet
ResultSet 持有数据库连接资源,必须在使用完毕后调用 rs.close():
// ✅ 正确:使用 try-finally 确保关闭
let rs: relationalStore.ResultSet | null = null
try {
rs = await store.query(pred, columns)
const results: User[] = []
if (rs.goToFirstRow()) {
do {
results.push(parseRow(rs))
} while (rs.goToNextRow())
}
return results
} finally {
rs?.close() // 无论成功还是异常,都关闭 ResultSet
}
7.2 列索引越界
获取列值时,应先检查列是否存在:
// ✅ 安全写法:先获取列索引
const colIndex = rs.getColumnIndex('email')
if (colIndex >= 0) {
const email = rs.getString(colIndex)
}
// ❌ 危险写法:直接用固定索引
const email = rs.getString(5) // 如果表结构变化,索引可能错位
7.3 getter 方法对照表
| 列类型 | 推荐 getter | 备注 |
|---|---|---|
| TEXT | getString(colIndex) |
返回 string |
| INTEGER | getLong(colIndex) |
返回 number |
| REAL | getDouble(colIndex) |
返回 number |
| BLOB | getBlob(colIndex) |
返回 Uint8Array |
| JSON字段 | getString(colIndex) + JSON.parse |
需要额外解析 |
如果这篇文章对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,你的支持是我持续创作的动力!
更多推荐

所有评论(0)