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 从查询到实体对象的完整解析流程

false,无数据

true,有数据

true,有下一行

false,遍历完毕

store.query / querySql

ResultSet 游标

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()  → 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
}

关键点:

  1. goToFirstRow() 返回 boolean——有数据时返回 true 并移动到第一行,无数据时返回 false
  2. 单条查询结果赋值给 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 的使用方法:

  1. 游标模型:ResultSet 像指针一样在行间移动,初始位置在 -1
  2. 遍历模式:第一条用 goToFirstRow() + 后续用 goToNextRow() 循环
  3. getter 选择:INTEGER→getLong(),TEXT→getString()
  4. parseRow 模式:将 ResultSet 当前行转换为 Entity 对象
  5. 资源管理rs.close() 必须在 return 之前调用
  6. 性能优化: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 需要额外解析

如果这篇文章对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,你的支持是我持续创作的动力!

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐