10.2 行数据入口
让每个入口对象对应数据源中的一行,封装该记录的定位与持久化操作。
学习目标
- 能为订单表设计一行一个对象的数据访问入口,并说明对象的加载、修改和保存生命周期
- 能用对象身份、脏状态和并发版本解释行数据入口与表数据入口的差异
- 能识别隐式写回、重复实例和跨行事务泄漏,并提出工作单元或数据映射器的迁移方案
为什么 10.2 行数据入口 值得单独学习
↡让数据源中的一行对应一个入口对象:对象保存该行的字段、知道自己的主键,并封装这行记录的插入、更新和删除。与一个对象代表整张表的表数据入口相比,它把“这一行如何定位和写回”变成对象自己的责任。
订单聚合持久化时,订单行可能在读取后被修改、校验和保存。若每次修改都直接拼 SQL,调用者必须知道主键、脏字段和更新顺序;若入口对象自动在任意 getter 中写回,调用者又很难预测何时产生数据库副作用。行数据入口要解决的是单行记录的访问边界,不是自动完成领域建模。
本章的订单行、入口生命周期、并发实验和练习均为课程原创;公开图书页与模式目录只用于核对 10.2 的模式名称及其与表数据入口、数据映射器的关系。学习重点是让对象状态和数据库行之间的转换可追踪、可测试、可撤回。
先建立直觉:一行对象何时代表哪一行
先猜一猜:同一个订单 ID 被两个调用路径加载时,两个入口对象是否可以同时代表同一行并各自保存?如果没有身份策略和并发检查,后保存的对象可能覆盖先保存的修改。行数据入口把便利的字段访问换成了生命周期责任,必须先说明对象何时被加载、修改和写回。
一个可审查的↡至少需要三类信息:当前字段快照、行主键和持久化状态。OrderRow 可以提供 changeQuantity()、save() 和 remove(),但 save() 的更新条件、失败结果和事务归属必须明确;对象不能因为持有一条数据就自动拥有订单折扣或授信规则。
它还要说明↡:同一个主键在一个工作单元内是否只能对应一个内存对象,跨请求是否重新加载,查询得到的列表对象是否进入同一身份范围。身份映射可以避免两个副本互相覆盖,但也会带来缓存失效、内存增长和并发语义,不能无条件开启。
目录单元到教学证据
10.2 行数据入口
在 10.2 行数据入口 的单元范围内,学习者要能画出“查询 → 行对象 → 字段变化 → 保存/删除”的生命周期,并区分表级入口负责批量查找、行入口负责单行状态。单元键为 poeaa24-pattern-06-row-data-gateway;核心证据是一次修改只影响目标行,且保存时的主键、版本和失败结果可被验证。
评审记录要回答五个问题:对象代表哪一行,主键何时绑定,字段修改何时变成脏状态,保存由谁调用,另一份快照同时保存时如何处理。如果这些答案只能依赖 ORM 的默认行为,说明行数据入口没有形成可解释的契约。
专属设计案例:订单行编辑
为 order_items 表设计 OrderItemRow。表数据入口先按订单 ID 查询多行,再为每一行创建入口对象;对象保存 itemId、数量和单价快照,changeQuantity() 只修改内存状态,save() 在明确的事务中写回目标行。订单总额和库存规则仍由更高层领域行为协调。
设计记录采用五个字段:模式键为 poeaa24-pattern-06-row-data-gateway;当前裁决是“调用者需要围绕单行生命周期进行修改和写回,且行对象状态可被明确管理时采用行数据入口”;观测项包括对象身份、脏状态、版本条件、写回时机和测试替身;保留条件是“对象只代表一行并能报告保存结果”;拒绝条件是“对象间共享大量隐式状态,或一次列表查询造成不可控的逐行写回”。
修改与保存必须分开
行对象的字段修改不应自动等于数据库更新。把修改留在内存中,可以让调用者先完成验证,再由工作单元统一提交;但对象必须有清楚的↡标记或变更记录,避免无变化的对象反复写回,也避免真正修改被静默忽略。
保存时应明确更新哪些列、使用哪个主键、预期影响几行以及失败如何返回。save() 可以只更新变化字段,也可以写回完整快照;选择取决于并发策略和数据库约束,不能让调用者从一次偶然的 ORM 日志猜出语义。
并发版本是契约的一部分
订单行被两个编辑页面同时打开时,后一个保存者可能覆盖先一个保存者的数量。使用版本列或更新时间作为条件,可以把“没有其他人修改过才允许写回”表达为↡检查;版本不匹配时返回冲突,而不是悄悄成功。行入口可以执行这个检查,但冲突后的合并或重试仍由应用流程决定。
模式结构图
用订单行走一遍入口生命周期
先绑定一行的身份
先预测:查询出三个订单行后,入口对象应该如何知道自己对应哪一行?为每个对象记录主键和字段快照,再检查列表调用者是否只把对象当成行记录使用,而不会把它误当成整张表或整个订单聚合。
常见误区
写回边界与测试替身
行入口可以通过显式方法暴露保存结果,也可以把变化交给更高层的工作单元统一提交。若使用工作单元,就要说明每个↡如何记录被注册的行对象、如何模拟版本冲突,以及一次提交失败后是否会继续写入其他对象。替身应验证协作,不应伪装成真实数据库。
行数据入口适合单行编辑、局部更新和对象生命周期清晰的场景;它不适合把数千行报表结果都变成长期对象。批量查询可以使用表数据入口完成,只有进入需要修改和保存的生命周期时,才创建行对象。这样能控制身份映射的范围和内存成本。
当字段、关联和行为开始跨越多张表时,行入口可能需要越来越多的加载规则。此时应评估数据映射器或领域模型,而不是继续给行对象增加隐藏查询。入口要保持可替换,领域调用不应依赖它内部是否使用 SQL、ORM 或缓存。
选择与拒绝矩阵
| 评审问题 | 选择行数据入口的证据 | 应拒绝或准备迁移的信号 |
|---|---|---|
| 身份 | 一个对象明确代表一行,有主键和生命周期 | 同一主键出现多个无协调副本 |
| 修改 | 字段变化、脏状态和保存时机可观察 | 读取或销毁对象隐式触发写回 |
| 并发 | 版本条件和冲突结果进入更新契约 | 后保存者无条件覆盖先保存者 |
| 批量 | 表入口负责查找,行入口只承载需要修改的行 | 列表查询把大量行变成长生命周期对象 |
| 替代 | 单行行为简单,工作单元可协调提交 | 多表关联和行为图复杂到需要专门映射层 |
本章小结
掌握 10.2 行数据入口的标志,不是让数据库行看起来像普通对象,而是能清楚说明一行对象的主键、字段快照、脏状态、保存边界和并发失败。它适合单行生命周期和局部更新;表级查询、跨行事务、领域规则和复杂对象图应留给更合适的协作者。通过重复实例、版本冲突、隐式写回和批量内存成本的证据,才能判断该入口是否仍然值得保留。
本章练习
练习
问题 1: 一个订单行对象被修改后,调用者还要执行库存校验。为什么不应在属性 setter 中立即写入数据库?
问题 2: 两个页面加载同一订单行,版本都是 7;第一个页面保存成功后版本变成 8,第二个页面随后保存。入口应该返回什么?
问题 3: 列表页一次加载一万条订单行,但只有两行需要编辑。是否应该为一万行都创建长期行入口对象?
前后导航
出处声明
名词解释
名词解释
本章出现的专业名词,用大白话再讲一遍。
- 行数据入口
- 让一个入口对象代表数据源中的一行,并封装该行的定位、修改和持久化操作。
- 行对象
- 保存一行数据快照、主键和持久化状态的入口对象。
- 对象身份
- 同一个数据库标识在内存中如何对应对象实例,以及实例生命周期如何管理的规则。
- 脏状态
- 对象字段已被修改但尚未写回数据源的状态或变更记录。
- 乐观并发
- 保存时携带版本或时间条件,发现其他修改后报告冲突而不是静默覆盖的策略。
- 测试替身
- 在测试中代替真实入口并记录调用、返回可控结果以验证协作的对象或实现。