鸿蒙应用开发实战【35】— DAO 层错误处理规范与单元测试
鸿蒙应用开发实战【35】— DAO 层错误处理规范与单元测试
本文是「号码助手全栈开发系列」第 35 篇,持续更新中…
开源社区:https://openharmonycrossplatform.csdn.net
前言
DAO 层处于 UI 和数据库之间,是所有数据请求的必经之路。一个未捕获的数据库异常会直接传递到 UI 层,导致应用崩溃或白屏。因此,完善的错误处理和充分的单元测试是 DAO 层质量的两个支柱。本篇将系统性地讲解错误处理规范和基于 Hypium 框架的 DAO 层单元测试策略。
本篇涵盖:错误类型分类(业务异常 vs 系统异常)、try-catch-finally 规范、DAO 层异常转换、ResultSet 泄漏防护、Hypium 单元测试框架入门、DAO 测试的 Mock 策略、测试用例编写模式。

一、错误类型分类
DAO 层的错误可以分为两大类:
1.1 业务异常
由业务规则约束引起的,应通过返回值或自定义错误传递:
| 场景 | 处理方式 |
|---|---|
| 查询的记录不存在 | 返回 null 或空数组 |
| 重复插入(唯一约束冲突) | 抛出语义化的 DuplicateError |
| 引用的 card_id 不存在 | 抛出 ForeignKeyViolationError |
| 状态转换不合法 | 返回跳过信息 |
1.2 异常处理策略对比
| 维度 | 业务异常 | 系统异常 |
|---|---|---|
| 来源 | 业务规则约束 | 基础设施故障 |
| 处理方式 | 返回值/null/自定义错误 | 记日志 + 向上抛出 |
| 给用户提示 | 具体操作建议 | 通用错误页 |
| 示例 | 记录不存在、重复插入 | 数据库损坏、磁盘满 |
| 是否影响其他功能 | 否 | 可能全局影响 |
由基础设施故障引起的,应记录日志并向上层抛出:
| 场景 | 处理方式 |
|---|---|
| 数据库文件损坏 | 捕获后记录日志,抛出到 UI 层显示错误页 |
| 磁盘空间不足 | 抛出 DatabaseError,提示用户清理空间 |
| 权限不足 | 安全级别配置错误,开发期应发现 |
| 并发写入冲突 | RDB 的 SQLite 引擎自动处理锁竞争 |
二、错误处理规范
2.1 基本 try-catch-finally 模式
所有数据库操作都应该遵循这个模式:
async getById(id: number): Promise<CardEntity | null> {
let rs: relationalStore.ResultSet | null = null;
try {
let predicates = new relationalStore.RdbPredicates('card');
predicates.equalTo('id', id);
rs = await this.store.query(predicates);
if (rs.rowCount === 0) return null;
rs.goToFirstRow();
return parseCard(rs);
} catch (error) {
console.error(`CardDao.getById failed: id=${id}`, error);
throw new DatabaseError('查询卡号失败', error);
} finally {
if (rs) {
rs.close();
}
}
}
三条铁律:
- ResultSet 必须在 finally 中关闭 — 无论查询成功还是异常,都要释放底层资源
- 捕获后不要吞异常 — 记日志后重新抛出,让 UI 层决定如何展示
- 异常转换 — 将通用 Error 转换为语义化的
DatabaseError
2.2 自定义错误类型
// DAO 层自定义错误基类
export class DatabaseError extends Error {
constructor(
message: string,
public readonly cause?: unknown
) {
super(message);
this.name = 'DatabaseError';
}
}
// 外键冲突
export class ForeignKeyViolationError extends DatabaseError {
constructor(
message: string,
public readonly entityType: string,
public readonly entityId?: number
) {
super(message);
this.name = 'ForeignKeyViolationError';
}
}
// 重复错误
export class DuplicateError extends DatabaseError {
constructor(
message: string,
public readonly field: string,
public readonly value: string
) {
super(message);
this.name = 'DuplicateError';
}
}
2.3 DAO 方法的异常策略
| 返回值类型 | 异常策略 | 示例 |
|---|---|---|
Promise<T | null> |
正常返回 null |
getById |
Promise<T[]> |
正常返回空数组 [] |
listAll |
Promise<number> |
正常返回 0 | delete / update |
Promise<void> |
失败抛出异常 | batchImport |
原则: 查询型方法(get/list)尽可能用返回值而非异常表示"无数据",命令型方法(insert/update/delete)在失败时抛出异常。
三、事务错误处理
3.1 事务的标准模式
async performSwitch(oldBindingId: number, newCardId: number): Promise<void> {
try {
this.store.beginTransaction();
// 业务操作
await this.updateStatus(oldBindingId, '待换绑');
await this.insert({ app_name: '微信', card_id: newCardId, status: '使用中' });
this.store.commit();
} catch (error) {
// 异常时回滚
try {
this.store.rollback();
} catch (rollbackError) {
console.error('Rollback also failed', rollbackError);
}
throw new DatabaseError('换绑操作失败', error);
}
}
⚠️ rollback 也需 try-catch:如果 commit 失败时数据库连接异常,rollback 也可能抛出错误。需要双重 try-catch 确保不会因为 rollback 异常而丢失原始错误信息。
3.2 避免事务嵌套
// ❌ 错误:DAO 内开启了事务,又被外部事务包裹
// 这会导致嵌套事务,RDB 的 SQLite 默认不支持嵌套
// ✅ 正确:让调用方控制事务边界,DAO 只做单表操作
// 如果需要跨表事务,在 Service 层统一管理
四、ResultSet 泄漏防护
ResultSet 泄漏是最容易犯的 DAO 层错误,下面是几条防护策略:
4.1 统一查询方法
// 所有 DAO 都使用统一的私有查询方法,确保 close
private async queryList(predicates: relationalStore.RdbPredicates): Promise<CardEntity[]> {
let rs = await this.store.query(predicates);
try {
let list: CardEntity[] = [];
while (rs.goToNextRow()) {
list.push(parseCard(rs));
}
return list;
} finally {
rs.close();
}
}
4.2 快速返回时注意 close
// ❌ 危险:提前 return 跳过了 close
async getById(id: number): Promise<CardEntity | null> {
let rs = await this.store.query(predicates);
if (rs.rowCount === 0) return null; // 没有 close!
rs.goToFirstRow();
const entity = parseCard(rs);
rs.close();
return entity;
}
// ✅ 安全:try-finally 包裹
async getById(id: number): Promise<CardEntity | null> {
let rs = await this.store.query(predicates);
try {
if (rs.rowCount === 0) return null;
rs.goToFirstRow();
return parseCard(rs);
} finally {
rs.close();
}
}
五、基于 Hypium 的单元测试
Hypium 是 HarmonyOS 的单元测试框架,支持 ArkTS 测试用例。
5.1 测试配置
在 entry/src/test/ 目录下创建测试文件:
// CardDao.test.ets
import { describe, it, expect } from '@ohos/hypium';
import { relationalStore } from '@kit.ArkData';
import { CardDao } from '../src/main/ets/dao/CardDao';
import { CardEntity, CardColor } from '../src/main/ets/entity/CardEntity';
export default function cardDaoTest() {
describe('CardDaoTest', () => {
// 测试前初始化数据库
let store: relationalStore.RdbStore;
beforeAll(async () => {
// 使用内存数据库
store = await relationalStore.getRdbStore(globalThis.context, {
name: 'test.db',
securityLevel: relationalStore.SecurityLevel.S1, // 测试用 S1
}, async () => {
await store.executeSql(`
CREATE TABLE IF NOT EXISTS card (
id INTEGER PRIMARY KEY AUTOINCREMENT,
label TEXT NOT NULL,
phone TEXT NOT NULL,
operator TEXT,
color TEXT DEFAULT '#4F7CFF',
sort_order INTEGER DEFAULT 0,
created_at TEXT DEFAULT (datetime('now', 'localtime')),
updated_at TEXT DEFAULT (datetime('now', 'localtime'))
)
`);
});
});
// 每个测试前清理数据
afterEach(async () => {
await store.executeSql('DELETE FROM card');
});
// 销毁数据库
afterAll(async () => {
await relationalStore.deleteRdbStore(globalThis.context, 'test.db');
});
});
}
5.2 测试用例编写
// 测试插入
it('insert_should_return_row_id', async () => {
const dao = new CardDao(store);
const id = await dao.insert({
label: '测试卡',
phone: '13800138000',
operator: '中国移动',
color: CardColor.BLUE,
});
expect(id).not().null();
expect(id).assertEqual(1); // 首次插入,id=1
});
// 测试查询
it('getById_should_return_entity', async () => {
const dao = new CardDao(store);
await dao.insert({ label: '测试卡', phone: '13800138000' });
const entity = await dao.getById(1);
expect(entity).not().null();
expect(entity!.label).assertEqual('测试卡');
expect(entity!.phone).assertEqual('13800138000');
});
// 测试不存在
it('getById_should_return_null_when_not_found', async () => {
const dao = new CardDao(store);
const entity = await dao.getById(999);
expect(entity).assertNull();
});
// 测试删除
it('delete_should_remove_entity', async () => {
const dao = new CardDao(store);
await dao.insert({ label: '测试卡', phone: '13800138000' });
const deleted = await dao.delete(1);
expect(deleted).assertEqual(1);
const entity = await dao.getById(1);
expect(entity).assertNull();
});
// 测试批量删除
it('deleteMany_should_remove_multiple', async () => {
const dao = new CardDao(store);
await dao.insert({ label: '卡1', phone: '111' });
await dao.insert({ label: '卡2', phone: '222' });
await dao.insert({ label: '卡3', phone: '333' });
const deleted = await dao.deleteMany([1, 3]);
expect(deleted).assertEqual(2);
const all = await dao.listAll();
expect(all.length).assertEqual(1);
expect(all[0].label).assertEqual('卡2');
});
5.3 测试事务回滚
it('transaction_should_rollback_on_error', async () => {
const dao = new CardDao(store);
try {
store.beginTransaction();
await dao.insert({ label: '卡1', phone: '111' });
await dao.insert({ label: '卡2', phone: '222' });
// 模拟异常
throw new Error('模拟错误');
// 下面的 commit 不会执行
store.commit();
} catch (error) {
store.rollback();
}
// 验证:事务回滚后,两条插入都应该不存在
const all = await dao.listAll();
expect(all.length).assertEqual(0);
});
5.5 测试覆盖清单
| 测试类别 | 用例示例 | 验证要点 |
|---|---|---|
| 正常流程 | insert / getById / update / delete | 基本 CRUD 正确性 |
| 边界条件 | 空数组、空关键词、不存在的 ID | DAO 层容错能力 |
| 异常流程 | 事务回滚、重复插入 | 错误处理正确性 |
| 数据一致性 | 导出→清空→导入→验证 | 导入导出完整性 |
5.4 测试边界条件
// 空数组批量操作
it('deleteMany_empty_ids_should_return_0', async () => {
const dao = new CardDao(store);
const deleted = await dao.deleteMany([]);
expect(deleted).assertEqual(0);
});
// 搜索空关键词
it('search_empty_keyword_should_return_all', async () => {
const dao = new CardDao(store);
await dao.insert({ label: '卡1', phone: '111' });
await dao.insert({ label: '卡2', phone: '222' });
const results = await dao.search('');
expect(results.length).assertEqual(2);
});
六、测试运行
# 在 DevEco Studio 中运行测试
# 或通过命令行
hvigorw runTest --module entry --testType hypium
Hypium 测试报告会显示每个用例的执行结果和耗时:
CardDaoTest
✓ insert_should_return_row_id (12ms)
✓ getById_should_return_entity (8ms)
✓ getById_should_return_null_when_not_found (5ms)
✓ delete_should_remove_entity (6ms)
✓ deleteMany_should_remove_multiple (10ms)
✓ transaction_should_rollback_on_error (4ms)
✓ deleteMany_empty_ids_should_return_0 (3ms)
7 passed (48ms)
小结
本篇完成了 DAO 层错误处理和单元测试的完整指南:
| 模块 | 关键实践 |
|---|---|
| 错误分类 | 业务异常(返回值) vs 系统异常(抛异常) |
| try-catch-finally | ResultSet 在 finally 中 close |
| 事务错误 | 双重 try-catch 保护 commit + rollback |
| 自定义错误 | DatabaseError / ForeignKeyViolationError |
| Hypium 测试 | 内存数据库 + beforeAll/afterEach |
| 测试用例 | insert / query / delete / batch / rollback |
| 边界测试 | 空数组、空关键词、不存在的 ID |
至此,DAO 层进阶系列(31-35)全部完成!接下来将进入数据库优化系列(36-40),深入查询优化与高级功能。
如果这篇文章对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,你的支持是我持续创作的动力!
相关资源:
- openHarmony 跨平台社区:https://openharmonycrossplatform.csdn.net
- HarmonyOS 官方文档:https://developer.huawei.com/consumer/cn/doc/
- HarmonyOS RDB开发指南:https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/data-persistence-by-rdb
- HarmonyOS hilog日志:https://developer.huawei.com/consumer/cn/doc/harmonyos-references/hilog
更多推荐




所有评论(0)