32 DAO 设计模式实战:完整的数据访问层封装
前言

图:32 DAO 设计模式实战:完整的数据访问层封装 运行效果截图(HarmonyOS NEXT)
DAO(Data Access Object) 是一种将数据访问逻辑与业务逻辑分离的设计模式。在"鹿鹿·笔迹心理分析"项目中,5 个 DAO 类构成了完整的数据访问层,封装了所有 RDB 操作,向上层页面提供简洁、类型安全的数据接口。
本文以 UserDao、HandwritingDao、ReportDao、ArchiveDao、RelationDao 为例,系统总结 DAO 层的代码组织结构、通用模式、异常处理和最佳实践。
鸿蒙官方·数据持久化指南:developer.huawei.com
项目源码仓库:harmony-app GitHub

图:DAO 层在应用架构中的位置------Page 层 → DAO 层 → DatabaseService → RdbStore
#mermaid-svg-hNGDbEXS3XruvBZl{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-hNGDbEXS3XruvBZl .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-hNGDbEXS3XruvBZl .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-hNGDbEXS3XruvBZl .error-icon{fill:#552222;}#mermaid-svg-hNGDbEXS3XruvBZl .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-hNGDbEXS3XruvBZl .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-hNGDbEXS3XruvBZl .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-hNGDbEXS3XruvBZl .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-hNGDbEXS3XruvBZl .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-hNGDbEXS3XruvBZl .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-hNGDbEXS3XruvBZl .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-hNGDbEXS3XruvBZl .marker{fill:#333333;stroke:#333333;}#mermaid-svg-hNGDbEXS3XruvBZl .marker.cross{stroke:#333333;}#mermaid-svg-hNGDbEXS3XruvBZl svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-hNGDbEXS3XruvBZl p{margin:0;}#mermaid-svg-hNGDbEXS3XruvBZl .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-hNGDbEXS3XruvBZl .cluster-label text{fill:#333;}#mermaid-svg-hNGDbEXS3XruvBZl .cluster-label span{color:#333;}#mermaid-svg-hNGDbEXS3XruvBZl .cluster-label span p{background-color:transparent;}#mermaid-svg-hNGDbEXS3XruvBZl .label text,#mermaid-svg-hNGDbEXS3XruvBZl span{fill:#333;color:#333;}#mermaid-svg-hNGDbEXS3XruvBZl .node rect,#mermaid-svg-hNGDbEXS3XruvBZl .node circle,#mermaid-svg-hNGDbEXS3XruvBZl .node ellipse,#mermaid-svg-hNGDbEXS3XruvBZl .node polygon,#mermaid-svg-hNGDbEXS3XruvBZl .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-hNGDbEXS3XruvBZl .rough-node .label text,#mermaid-svg-hNGDbEXS3XruvBZl .node .label text,#mermaid-svg-hNGDbEXS3XruvBZl .image-shape .label,#mermaid-svg-hNGDbEXS3XruvBZl .icon-shape .label{text-anchor:middle;}#mermaid-svg-hNGDbEXS3XruvBZl .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-hNGDbEXS3XruvBZl .rough-node .label,#mermaid-svg-hNGDbEXS3XruvBZl .node .label,#mermaid-svg-hNGDbEXS3XruvBZl .image-shape .label,#mermaid-svg-hNGDbEXS3XruvBZl .icon-shape .label{text-align:center;}#mermaid-svg-hNGDbEXS3XruvBZl .node.clickable{cursor:pointer;}#mermaid-svg-hNGDbEXS3XruvBZl .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-hNGDbEXS3XruvBZl .arrowheadPath{fill:#333333;}#mermaid-svg-hNGDbEXS3XruvBZl .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-hNGDbEXS3XruvBZl .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-hNGDbEXS3XruvBZl .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-hNGDbEXS3XruvBZl .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-hNGDbEXS3XruvBZl .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-hNGDbEXS3XruvBZl .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-hNGDbEXS3XruvBZl .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-hNGDbEXS3XruvBZl .cluster text{fill:#333;}#mermaid-svg-hNGDbEXS3XruvBZl .cluster span{color:#333;}#mermaid-svg-hNGDbEXS3XruvBZl div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-hNGDbEXS3XruvBZl .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-hNGDbEXS3XruvBZl rect.text{fill:none;stroke-width:0;}#mermaid-svg-hNGDbEXS3XruvBZl .icon-shape,#mermaid-svg-hNGDbEXS3XruvBZl .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-hNGDbEXS3XruvBZl .icon-shape p,#mermaid-svg-hNGDbEXS3XruvBZl .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-hNGDbEXS3XruvBZl .icon-shape .label rect,#mermaid-svg-hNGDbEXS3XruvBZl .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-hNGDbEXS3XruvBZl .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-hNGDbEXS3XruvBZl .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-hNGDbEXS3XruvBZl :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 页面层 Page
UserDao
HandwritingDao
ArchiveDao
ReportDao
RelationDao
DatabaseService 单例
RdbStore
SQLite 引擎
一、DAO 层的整体架构
1.1 包结构
features/data/
├── DatabaseService.ets ← 数据库初始化、版本管理(单例)
├── Models.ets ← 实体类型定义(5 个接口)
├── UserDao.ets ← 用户表操作
├── ArchiveDao.ets ← 档案表操作
├── HandwritingDao.ets ← 笔迹表操作
├── ReportDao.ets ← 报告表操作
├── RelationDao.ets ← 关系表操作
└── DemoSeeder.ets ← Demo 数据播种器
1.2 各 DAO 的职责和 CRUD 覆盖
| DAO | 表名 | CREATE | READ | UPDATE | DELETE | 特有方法 |
|---|---|---|---|---|---|---|
| UserDao | user | ✅ ensureLocalUser | ✅ findByOpenId | ✅ updateName | ❌ | 查找/创建本地用户 |
| ArchiveDao | archive | ✅ create | ✅ listByUser / findById | ✅ incrementRecord | ✅ delete(级联) | 计数更新 |
| HandwritingDao | handwriting | ✅ create | ✅ findById / listByArchive / listUnclassified / listRecent | ✅ batchUpdateArchive | ❌ | 统计方法(countAll/countByArchive/countDistinctActiveDays) |
| ReportDao | report | ✅ create | ✅ findById / findByHandwriting / listByArchive / listRecent / findLatestByArchive | ❌ | ❌ | parseRadar / parseTags 工具方法 |
| RelationDao | relation | ✅ create | ✅ findByUser / listByUser | ✅ updateMatchScore | ✅ delete | 关系绑定管理 |
二、DAO 的标准代码结构
2.1 常量定义
每个 DAO 文件都有统一的常量和工具方法:
typescript
const TAG = 'HandwritingDao' // 日志标签
const TABLE = 'handwriting' // 表名
// 查询列名数组(用于 store.query 的第二个参数)
const COLS: string[] = [
'id', 'archive_id', 'source', 'image_path', 'ocr_text',
'feature_json', 'energy_score', 'device_type',
'word_count', 'write_duration', 'created_at'
]
2.2 getStore 工具方法
typescript
private static getStore(): relationalStore.RdbStore {
return DatabaseService.getInstance().getStore()
}
所有 DAO 通过这种方式获取 RdbStore 实例,如果数据库未初始化,getStore() 会抛出明确异常。
2.3 完整的 DAO 类骨架
typescript
export class HandwritingDao {
// ① 工具方法
private static getStore(): relationalStore.RdbStore { ... }
// ② CREATE 方法
static async create(item: HandwritingEntity): Promise<number> { ... }
// ③ READ 方法(单条)
static async findById(id: number): Promise<HandwritingEntity | null> { ... }
// ④ READ 方法(列表)
static async listByArchive(archiveId: number): Promise<HandwritingEntity[]> { ... }
// ⑤ UPDATE 方法
static async batchUpdateArchive(ids: number[], archiveId: number): Promise<void> { ... }
// ⑥ DELETE 方法(如果有)
// static async delete(id: number): Promise<void> { ... }
// ⑦ 聚合统计方法
static async countByArchive(archiveId: number): Promise<number> { ... }
// ⑧ 私有解析方法
private static parseRow(rs: relationalStore.ResultSet): HandwritingEntity { ... }
}
三、静态方法 vs 实例方法
3.1 项目中所有 DAO 方法都是 static
typescript
// ✅ 项目中:所有 DAO 方法都是静态的
const count = await HandwritingDao.countAll()
const report = await ReportDao.findByHandwriting(hwId)
// 不创建 DAO 实例
// const dao = new HandwritingDao() ❌ 不需要
静态方法的优势:
| 优势 | 说明 |
|---|---|
| 无状态 | 每次调用独立,没有内部状态需要维护 |
| 无需实例化 | 减少对象创建开销 |
| 直接使用 | await DAO.method() 即可 |
四、统一异常处理模式
4.1 三级异常处理
typescript
export class HandwritingDao {
static async create(item: HandwritingEntity): Promise<number> {
try {
const store = HandwritingDao.getStore()
const values: relationalStore.ValuesBucket = { /* ... */ }
return await store.insert(TABLE, values)
} catch (e) {
// ① 日志记录
hilog.error(0x0000, 'HandwritingDao', 'create 失败: %s', JSON.stringify(e))
// ② 向上抛出
throw new Error('create 失败: ' + JSON.stringify(e))
}
}
}
4.2 业务调用方的异常处理
typescript
// DemoSeeder --- 调用 DAO 的异常处理
try {
const hwId = await HandwritingDao.create(item)
await ReportDao.create(report)
} catch (e) {
hilog.error(0x0000, 'DemoSeeder', '播种失败: %s', JSON.stringify(e))
// 播种失败不阻塞主流程
}
五、数据流图
5.1 完整的数据流
Page(UI 层)
↓ 调用 DAO 方法
DAO(数据访问层)
↓ DatabaseService.getStore()
RdbStore
↓ query / insert / update / delete
RDB 数据库
↓ ResultSet
DAO.parseRow()
↓ Entity 对象
Page 使用数据渲染 UI
5.2 调用示例
typescript
// CapturePage --- 拍摄完成后的数据流
// ① 页面调用 DAO
const handwritingId = await HandwritingDao.create({
archive_id: currentArchiveId,
source: 'camera',
image_path: savedPath,
ocr_text: ocrResult,
created_at: Date.now()
})
// ② ReportDao 创建关联报告
await ReportDao.create({
handwriting_id: handwritingId,
archive_id: currentArchiveId,
personality_type: analysisResult.type,
radar_json: JSON.stringify(analysisResult.scores),
created_at: Date.now()
})
// ③ ArchiveDao 更新计数
await ArchiveDao.incrementRecord(currentArchiveId, Date.now())
// ④ 页面跳转到分析结果页
this.getUIContext().getRouter().pushUrl({ url: 'pages/ReportDetailPage' })
六、DAO 方法的命名规范
6.1 项目中的命名约定
| 前缀 | 含义 | 示例 |
|---|---|---|
create |
插入单条 | HandwritingDao.create(item) |
findById |
按 ID 查单条 | ArchiveDao.findById(id) |
findBy* |
按其他字段查单条 | UserDao.findByOpenId(openId) |
listBy* |
按条件查列表 | ReportDao.listByArchive(archiveId) |
listRecent |
最近列表 | HandwritingDao.listRecent(limit) |
list* |
通用列表 | ArchiveDao.listByUser(userId) |
count* |
计数 | HandwritingDao.countAll() |
update* |
更新 | UserDao.updateName(id, name) |
batchUpdate* |
批量更新 | HandwritingDao.batchUpdateArchive(ids, archiveId) |
delete |
删除 | ArchiveDao.delete(id) |
七、DAO 层的职责边界
7.1 DAO 应该做的事
typescript
// ✅ DAO 应该:封装数据访问
static async findLatestByArchive(archiveId: number): Promise<ReportEntity | null>
// 只负责"从 report 表查询档案最新报告"
// ✅ DAO 应该:类型安全的参数和返回值
// 输入:number → 输出:ReportEntity | null
// ✅ DAO 应该:JSON 序列化/反序列化
// 在 Entity 和 RDB 之间转换
7.2 DAO 不应该做的事
typescript
// ❌ DAO 不应该:包含业务逻辑
static async shouldShowOnboarding(userId: number): Promise<boolean> {
const count = await this.countAll()
return count < 3 // 这是业务判断,应该在 service 层
}
// ❌ DAO 不应该:包含 UI 逻辑
static async getName(): Promise<string> {
const user = await this.findByOpenId('local')
return user?.name ?? '默认用户' // UI 默认值的处理应在页面上
}
// ❌ DAO 不应该:调用其他 DAO(数据一致性应由业务层协调)
// 正确的做法:DemoSeeder 是业务层,负责协调多个 DAO
7.3 DAO 与业务层的边界
DemoSeeder ← 业务层(协调多个 DAO)
├── ArchiveDao.create()
├── HandwritingDao.create()
└── ReportDao.create()
EntryAbility ← 应用入口(初始化数据库)
└── DatabaseService.init(context)
Pages ← UI 层(调用 DAO 获取数据)
├── HomePage → HandwritingDao.listRecent()
└── ReportDetailPage → ReportDao.findByHandwriting()
八、DAO 模式的优势总结
| 维度 | 直接使用 RdbStore | 通过 DAO 封装 |
|---|---|---|
| 重复代码 | 每个页面重复写 insert/query | 一次封装,多处调用 |
| 类型安全 | ResultSet → 手动解析 | Entity 接口约束 |
| 可维护性 | 表结构变更 → 所有页面都要改 | 改 DAO 的一处 parseRow |
| 测试性 | 每个页面需要 mock | DAO 可以独立测试 |
| 安全性 | 可能注入 | 参数化绑定已封装 |
总结
本文系统总结了"鹿鹿"项目中 DAO 设计模式的实践:
- 5 个 DAO:UserDao、ArchiveDao、HandwritingDao、ReportDao、RelationDao
- CRUD 覆盖:每个 DAO 覆盖常用的 3-5 个数据操作
- 标准结构:常量 → getStore → 静态方法 → parseRow
- 异常处理:日志记录 + 向上抛出,不在 DAO 中吞没异常
- 数据流:Page → DAO → RdbStore → DB → Entity
- 职责边界:DAO 只负责数据访问,业务逻辑由上层协调
下一篇文章将介绍 Demo 数据播种器------让应用首次打开就有内容的秘密武器。
如果这篇文章对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,你的支持是我持续创作的动力!
参考资源:
- DAO 设计模式概览
- UserDao 项目源码(file:///Users/fiona/Downloads/bijixinli/harmony-app/entry/src/main/ets/features/data/UserDao.ets)
- HandwritingDao 项目源码(file:///Users/fiona/Downloads/bijixinli/harmony-app/entry/src/main/ets/features/data/HandwritingDao.ets)
- ArchiveDao 项目源码(file:///Users/fiona/Downloads/bijixinli/harmony-app/entry/src/main/ets/features/data/ArchiveDao.ets)
- ReportDao 项目源码(file:///Users/fiona/Downloads/bijixinli/harmony-app/entry/src/main/ets/features/data/ReportDao.ets)
- RelationDao 项目源码(file:///Users/fiona/Downloads/bijixinli/harmony-app/entry/src/main/ets/features/data/RelationDao.ets)
- DatabaseService 项目源码(file:///Users/fiona/Downloads/bijixinli/harmony-app/entry/src/main/ets/features/data/DatabaseService.ets)
- Models.ets 实体定义(file:///Users/fiona/Downloads/bijixinli/harmony-app/entry/src/main/ets/features/data/Models.ets)
- HarmonyOS 开发文档
- hypium 单元测试框架
- DAO 设计模式 Wiki
- Repository 模式详解
七、DAO 测试与 Mock 策略
7.1 单元测试 DAO 方法
在 HarmonyOS 中,可以使用 @ohos/hypium 测试框架对 DAO 方法进行单元测试:
typescript
// DAO 单元测试示例
import { describe, it, expect, beforeAll } from '@ohos/hypium'
import { UserDao } from '../../../main/ets/features/data/UserDao'
import { DatabaseService } from '../../../main/ets/features/data/DatabaseService'
export default function userDaoTest() {
describe('UserDaoTest', () => {
beforeAll(async () => {
// 初始化测试数据库
await DatabaseService.getInstance().init(getContext())
})
it('should create and find user', async () => {
const testUser = {
id: 'test_user_001',
nickname: '测试用户',
avatar: '',
createdAt: new Date().toISOString()
}
await UserDao.create(testUser)
const found = await UserDao.findById(testUser.id)
expect(found?.nickname).assertEqual('测试用户')
})
it('should delete user', async () => {
await UserDao.delete('test_user_001')
const found = await UserDao.findById('test_user_001')
expect(found).assertNull()
})
})
}
7.2 DAO 错误处理规范
项目中所有 DAO 方法遵循统一的错误处理规范:
| 场景 | 处理方式 | 返回值 |
|---|---|---|
| 查询无结果 | 不抛异常 | null 或 [] |
| 插入失败 | 抛出异常 | throw new Error(msg) |
| 更新失败 | 抛出异常 | throw new Error(msg) |
| 删除失败 | 抛出异常 | throw new Error(msg) |
| 数据库未初始化 | 抛出异常 | Database not initialized |
7.3 DAO 与 Repository 模式的对比
| 模式 | 特点 | 适用场景 |
|---|---|---|
| DAO | 直接对应数据库操作,一张表一个 DAO | 简单 CRUD,数据层薄 |
| Repository | 面向领域对象,可跨多表/多数据源 | 复杂业务逻辑,需要缓存 |
| ActiveRecord | 实体类自带 save/delete 方法 | 快速开发,简单模型 |
本项目使用 DAO 模式,代码简洁,易于维护。对于更复杂的鸿蒙项目,可以考虑在 DAO 之上加一层 Repository 做缓存和数据聚合。
你在项目中使用哪种数据访问模式?
- A. 纯 DAO(直接操作数据库)
- B. DAO + Repository 双层
- C. ActiveRecord(实体类带操作方法)
- D. ORM 框架(如 TypeORM)
欢迎在评论区分享你的架构选择和经验!
系列文章回顾: 本文是"鸿蒙 RDB 数据库"系列的第 9 篇,前几篇已覆盖 DatabaseService 初始化、表结构设计、版本升级、RdbPredicates 查询、ValuesBucket 插入、ResultSet 解析等全流程。
下一篇: 将介绍 DemoSeeder 数据播种器,让应用首次启动时拥有完整的演示数据。
如果这篇文章对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,你的支持是我持续创作的动力!