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

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 设计模式的实践:

  1. 5 个 DAO:UserDao、ArchiveDao、HandwritingDao、ReportDao、RelationDao
  2. CRUD 覆盖:每个 DAO 覆盖常用的 3-5 个数据操作
  3. 标准结构:常量 → getStore → 静态方法 → parseRow
  4. 异常处理:日志记录 + 向上抛出,不在 DAO 中吞没异常
  5. 数据流:Page → DAO → RdbStore → DB → Entity
  6. 职责边界: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 数据播种器,让应用首次启动时拥有完整的演示数据。
如果这篇文章对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,你的支持是我持续创作的动力!

相关推荐
绝世番茄1 小时前
Column 与 if/else 条件渲染:动态子组件的布局行为
华为·harmonyos·鸿蒙
花开彼岸天~1 小时前
鸿蒙实战:AppStorage 全局状态管理
华为·harmonyos·鸿蒙系统
爱吃大芒果2 小时前
鸿蒙 7 新特性科普:什么是系统 Agent 智能体?
华为·harmonyos·智能体
listening7772 小时前
HarmonyOS 6.1 元服务实战:把电商卡片装进系统“负一屏”
华为·harmonyos
蓝速科技2 小时前
蓝速科技鸿蒙信创终端全场景落地实效展示
人工智能·科技·华为·harmonyos
子非鱼94272 小时前
10-Flutter调用鸿蒙原生能力前置课:MethodChannel和PlatformView准备
学习·flutter·华为·harmonyos
xyccstudio3 小时前
用 AI 审核 Skill 消灭“编译器不报错“的隐藏 Bug
harmonyos
星释3 小时前
鸿蒙智能体开发实战:38.鸿蒙壁纸大师 - 根据设备型号优化壁纸尺寸
华为·harmonyos
xd1855785553 小时前
鸿蒙PC时代的AI快递查询应用:基于鸿蒙Flutter框架的跨端创新实践
人工智能·flutter·华为·harmonyos·鸿蒙
FrameNotWork3 小时前
HarmonyOS 6.1 富文本与图文混排 — Span/ContainerSpan/ImageSpan组合拳
华为·harmonyos