10.1 表数据入口
用一个对象封装整张表的数据访问,一个实例负责该表所有行的查询、插入、更新和删除。
学习目标
- 能为订单表设计集中查询、插入、更新和删除的数据访问入口
- 能区分表数据入口的 SQL 隔离、对象映射和领域规则责任
- 能依据查询复杂度、批量需求、对象身份和测试成本判断何时迁移到行数据入口或数据映射器
为什么 10.1 表数据入口 值得单独学习
↡是一个以数据库表为边界的数据访问对象:一个入口负责该表所有行的查询、插入、更新和删除。它把 SQL、参数绑定和结果映射集中到一处,让上层调用者不必知道列名、连接细节和写回顺序。
订单聚合持久化时,应用可能需要查找订单、批量加载订单行、更新状态和删除过期记录。如果每个用例都直接拼接 SQL,列的变化会扩散到控制器、批处理和领域服务。表数据入口先解决的是数据访问的组织问题,不承诺替代领域模型,也不自动提供跨表事务。
本章的订单表、入口契约、结构图、实验和练习均为课程原创;公开图书页与模式目录只用于核对 10.1 的模式名称和数据源模式范围。学习重点是观察数据访问边界如何降低耦合,以及它在哪些复杂度下开始失去清晰度。
先建立直觉:让一张表有一个可追踪入口
先猜一猜:如果订单列表页、退款任务和后台导入都需要查询订单,应该让每个调用者直接写 SQL,还是共享一个表级入口?共享入口的收益是 SQL 变化集中、参数规则一致、查询可以被统一记录;代价是一个入口可能逐渐承载过多查询形状,需要重新划分职责。
表数据入口的核心是↡,不是把每条 SQL 换成一个名字含糊的方法。OrderGateway.findById() 应该说明返回什么、找不到如何表示、是否包含订单行;OrderGateway.updateStatus() 应该明确更新条件和受影响行数。方法名和返回结果共同构成可审查的边界。
它也要说明↡如何处理:同一个订单 ID 在一次工作流里被查询两次,是返回同一个内存对象、两个快照,还是只返回数据记录?表数据入口通常只负责行与数据对象的访问,不自动维护全局身份映射;调用者必须知道这个选择,避免把两个副本的修改互相覆盖。
目录单元到教学证据
10.1 表数据入口
在 10.1 表数据入口 的单元范围内,学习者要能把“领域调用 → 表级入口 → SQL → 行映射 → 结果”画成可追踪链路,并证明调用者不再依赖列名和数据库连接细节。单元键为 poeaa24-pattern-05-table-data-gateway;核心证据是订单表字段变化时,业务用例无需同步修改所有查询。
评审记录要回答五个问题:一个入口负责哪张表,查询方法返回什么,插入和更新如何报告失败,行映射在哪里发生,跨表操作由谁协调。如果入口开始决定折扣和授信规则,或每个方法返回不同的匿名结构,说明数据边界和领域边界正在混淆。
专属设计案例:订单聚合持久化
为 orders 表设计 OrderGateway:findById() 查询单个订单,findOpenByCustomer() 查询客户的未完成订单,insert() 写入新订单,updateStatus() 更新状态,deleteExpired() 执行批量清理。入口负责参数绑定、列到数据对象的转换和数据库错误归一化;订单是否允许提交仍由领域行为决定。
设计记录采用五个字段:模式键为 poeaa24-pattern-05-table-data-gateway;当前裁决是“表结构稳定、多个用例共享同一张表的访问规则时采用表数据入口”;观测项包括表级封装、对象身份、查询形状、映射成本和测试替身;保留条件是“SQL 变化集中且方法契约可测试”;拒绝条件是“查询组合复杂到入口无法说明返回结构,或业务规则开始依赖数据库行副作用”。
入口契约比 SQL 细节更重要
一个入口方法应该暴露业务调用者真正需要的结果,而不是暴露游标、连接或 ORM 查询构造器。findOpenByCustomer() 可以返回只读的订单记录集合;它不应要求控制器理解连接生命周期,也不应让调用者自行拼接状态条件。SQL 仍然存在,但它被约束在一个能被替换和验证的端口之后。
单行访问与批量访问分开说明
单个订单的读取、多个订单的列表查询和过期订单的↡有不同的成本与失败语义。批量删除需要报告受影响行数和分页策略,不能简单地循环调用单行删除;列表查询要说明排序、分页和重复行处理。入口可以集中这些差异,但必须让契约把它们说清楚。
模式结构图
用订单表走一遍数据入口设计
先划出一张表的访问边界
先预测:订单列表页和退款任务都需要订单数据时,哪些 SQL 细节应该留在 OrderGateway 内?先列出查询、插入、更新和删除四类操作,再检查调用者是否只依赖方法契约和结果,而不依赖列名与连接对象。
常见误区
映射、身份与测试替身
表数据入口应有清楚的↡:哪些列进入数据对象,空值如何表示,数据库类型如何转换,未知列是否被忽略。映射契约稳定后,表结构调整可以在入口内部消化;如果上层直接依赖行数组位置,入口就没有真正隔离变化。
当订单数据需要跨多个查询保持一致时,可以由更高层的工作单元管理身份和写回顺序。不要为了给表入口增加全局缓存而隐式改变生命周期;缓存命中、失效和并发覆盖都应该是明确的设计决定。
测试时可以用↡返回固定订单记录,验证领域调用是否使用了正确的入口方法、是否处理找不到和冲突结果。替身测试不能证明 SQL 正确,因此仍需要针对真实数据库的集成测试,覆盖列映射、索引、分页和受影响行数。
选择与拒绝矩阵
| 评审问题 | 选择表数据入口的证据 | 应拒绝或准备迁移的信号 |
|---|---|---|
| 表边界 | 一个入口集中一张表的读写,方法契约稳定 | 一个入口跨越多张表并隐藏完整业务流程 |
| 查询 | 查询形状有限,SQL 变化可局部消化 | 动态组合、报表和聚合查询让返回结构不可说明 |
| 映射 | 列到数据对象的转换集中且可测试 | 上层依赖列位置、匿名行结构或 ORM 细节 |
| 身份 | 明确快照、缓存和批量结果的生命周期 | 同一 ID 的副本互相覆盖,身份策略靠偶然行为 |
| 替代 | 多表事务由服务层或工作单元协调 | 入口必须处理跨聚合一致性和复杂对象图 |
本章小结
掌握 10.1 表数据入口的标志,不是把 SQL 都搬进一个类,而是能解释一张表的访问边界、方法契约、行映射、对象身份和批量失败语义。它适合集中稳定的表级读写,不负责领域规则,也不自动解决跨表事务和全局身份。通过找不到、冲突、分页、批量和重复读取等证据,才能判断入口是否仍然比更复杂的映射结构更合适。
本章练习
练习
问题 1: 请为订单表设计三个表数据入口方法,并说明它们返回结果时不能把什么数据库细节泄漏给调用者。
问题 2: 同一订单在一次请求中被两次查询,得到两个内存副本,后一次保存覆盖了前一次修改。应该如何评审?
问题 3: 订单与授信账户需要在同一业务操作中更新。是否应该让订单表入口直接调用授信表入口并完成整个流程?
前后导航
出处声明
名词解释
名词解释
本章出现的专业名词,用大白话再讲一遍。
- 表数据入口
- 以一张数据库表为边界,集中负责该表所有行查询和写入的数据访问对象。
- 表级封装
- 把一张表的 SQL、参数绑定、结果映射和写回规则集中在一个可调用入口之后。
- 对象身份
- 同一个业务标识在内存中是否代表同一个对象,以及该对象生命周期如何管理的规则。
- 批量操作
- 一次处理多行数据的查询、更新或删除方式,需要明确分页、受影响行数和失败语义。
- 映射契约
- 数据库列、空值、类型与数据对象字段之间稳定且可测试的转换约定。
- 测试替身
- 在测试中替代真实数据入口、返回可控结果以验证调用协作的对象或实现。