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 日志猜出语义。

并发版本是契约的一部分

订单行被两个编辑页面同时打开时,后一个保存者可能覆盖先一个保存者的数量。使用版本列或更新时间作为条件,可以把“没有其他人修改过才允许写回”表达为检查;版本不匹配时返回冲突,而不是悄悄成功。行入口可以执行这个检查,但冲突后的合并或重试仍由应用流程决定。

模式结构图

Row Data Gateway:一个对象 = 一行OrderRow (实例 = 一行)id: numbercustomerId: numberamount: numberstatus: string+ save(): void+ delete(): void✓ 对象字段 = 表列,一一对应✗ 无业务逻辑,只是数据容器每个实例精确对应一行,字段即列,方法只有 save/delete
Row Data Gateway 让一个对象实例精确对应数据库中的一行。 对象持有字段值并提供 save/delete 方法,但不包含业务逻辑。

用订单行走一遍入口生命周期

分步1 / 3

先绑定一行的身份

先预测:查询出三个订单行后,入口对象应该如何知道自己对应哪一行?为每个对象记录主键和字段快照,再检查列表调用者是否只把对象当成行记录使用,而不会把它误当成整张表或整个订单聚合。

Row Data Gateway:一个对象 = 一行OrderRow (实例 = 一行)id: numbercustomerId: numberamount: numberstatus: string+ save(): void+ delete(): void✓ 对象字段 = 表列,一一对应✗ 无业务逻辑,只是数据容器每个实例精确对应一行,字段即列,方法只有 save/delete
Row Data Gateway 让一个对象实例精确对应数据库中的一行。 对象持有字段值并提供 save/delete 方法,但不包含业务逻辑。

常见误区

写回边界与测试替身

行入口可以通过显式方法暴露保存结果,也可以把变化交给更高层的工作单元统一提交。若使用工作单元,就要说明每个如何记录被注册的行对象、如何模拟版本冲突,以及一次提交失败后是否会继续写入其他对象。替身应验证协作,不应伪装成真实数据库。

行数据入口适合单行编辑、局部更新和对象生命周期清晰的场景;它不适合把数千行报表结果都变成长期对象。批量查询可以使用表数据入口完成,只有进入需要修改和保存的生命周期时,才创建行对象。这样能控制身份映射的范围和内存成本。

当字段、关联和行为开始跨越多张表时,行入口可能需要越来越多的加载规则。此时应评估数据映射器或领域模型,而不是继续给行对象增加隐藏查询。入口要保持可替换,领域调用不应依赖它内部是否使用 SQL、ORM 或缓存。

选择与拒绝矩阵

评审问题选择行数据入口的证据应拒绝或准备迁移的信号
身份一个对象明确代表一行,有主键和生命周期同一主键出现多个无协调副本
修改字段变化、脏状态和保存时机可观察读取或销毁对象隐式触发写回
并发版本条件和冲突结果进入更新契约后保存者无条件覆盖先保存者
批量表入口负责查找,行入口只承载需要修改的行列表查询把大量行变成长生命周期对象
替代单行行为简单,工作单元可协调提交多表关联和行为图复杂到需要专门映射层

本章小结

掌握 10.2 行数据入口的标志,不是让数据库行看起来像普通对象,而是能清楚说明一行对象的主键、字段快照、脏状态、保存边界和并发失败。它适合单行生命周期和局部更新;表级查询、跨行事务、领域规则和复杂对象图应留给更合适的协作者。通过重复实例、版本冲突、隐式写回和批量内存成本的证据,才能判断该入口是否仍然值得保留。

本章练习

练习

问题 1: 一个订单行对象被修改后,调用者还要执行库存校验。为什么不应在属性 setter 中立即写入数据库?

问题 2: 两个页面加载同一订单行,版本都是 7;第一个页面保存成功后版本变成 8,第二个页面随后保存。入口应该返回什么?

问题 3: 列表页一次加载一万条订单行,但只有两行需要编辑。是否应该为一万行都创建长期行入口对象?

前后导航

出处声明

名词解释

名词解释

本章出现的专业名词,用大白话再讲一遍。

行数据入口
让一个入口对象代表数据源中的一行,并封装该行的定位、修改和持久化操作。
行对象
保存一行数据快照、主键和持久化状态的入口对象。
对象身份
同一个数据库标识在内存中如何对应对象实例,以及实例生命周期如何管理的规则。
脏状态
对象字段已被修改但尚未写回数据源的状态或变更记录。
乐观并发
保存时携带版本或时间条件,发现其他修改后报告冲突而不是静默覆盖的策略。
测试替身
在测试中代替真实入口并记录调用、返回可控结果以验证协作的对象或实现。

讨论

评论区加载中…