第3章 映射到关系数据库

系统处理对象身份、关系、加载、更新、继承、元数据和连接生命周期的阻抗失配。

学习目标

  • 能解释对象身份、关系、继承、行为与关系数据库行列之间的阻抗失配
  • 能编写 TypeScript 映射器与工作单元,完成订单聚合的读取、变更跟踪和事务写回
  • 能根据领域复杂度、查询形状、连接生命周期和维护成本,选择或拒绝映射策略

为什么第3章映射到关系数据库值得单独学习

系统处理对象身份、关系、加载、更新、继承、元数据和连接生命周期的阻抗失配。第3章映射到关系数据库的核心不是套用某个框架 API,而是回答:如何在对象身份、关联、继承和写回之间保住一致语义,同时控制查询与映射成本。在订单聚合持久化中,如果无法说清责任、状态和失败由谁承担,即使正常请求能够返回,架构决定也没有完成。

不是“数据库不好”或“对象更高级”,而是需要被明确管理的边界。本页以 2024 年中文版公开目录限定 第3章 映射到关系数据库 的范围,并依据 Martin Fowler 的作者图书页和模式目录独立重写。它不复现原书正文、插图或代码;目录只决定“要讲什么”,这里的案例、实验、判断题和答案均为本课程原创。

先建立直觉:问题、机制与代价

第3章映射到关系数据库面对的具体压力是“同一身份实例数、查询次数、脏对象集合与关联装载范围”。它采用的机制可以概括为:先定义对象与关系的边界,再按复杂度选择主动记录、数据映射器、身份映射和事务协作者。机制带来的收益必须与新增间接层、同步责任和迁移成本同时记录,否则学习者只会得到一个没有拒绝条件的模式名称。

对第3章映射到关系数据库,优先比较的替代路线是:直接查询投影、活动记录、数据映射器、工作单元及结构映射组合。只有当对象模型需要独立于表结构演化,且一致性与身份需要跨多行维护时,才值得增加映射层;若只是为简单报表增加,就不能据此选择复杂方案。

目录单元到教学证据

第3章映射到关系数据库

第3章映射到关系数据库 的学习边界里,第3章映射到关系数据库不是待背诵的目录词,而是用来检查对象图是否把责任交给正确对象。对订单聚合持久化,学习者要记录身份、关系和连接边界的可观察变化,并说明它何时支持或否定本章结论。

3.1 架构模式

比较主动记录、表数据入口、行数据入口与数据映射器:映射边界应由对象复杂度和替换需求决定。第3章的架构模式选择先看对象是否需要独立行为,再决定是否引入映射器、身份映射和工作单元;简单 CRUD 不应自动升级成完整对象图。

3.2 行为问题

观察继承、关联、多态和粒度,确认数据库结构不会悄悄决定领域行为。行为问题的证据是同一个领域动作在换表结构后仍保持语义;如果业务方法开始围绕列名分支,就说明关系模型已经越过映射边界。

3.3 读取数据

区分查询投影、延迟加载和身份复用,记录一次请求到底装载了多少对象。读取数据时要同时记录往返次数、对象数量和缺少关联的语义,避免为了复用对象而加载整个数据库,也避免为了省查询而丢掉身份一致性。

3.4 结构映射模式

把表行、外键、主键和对象关系写成显式映射契约,避免 ORM 默认规则成为架构。结构映射模式应说明每个关系列如何进入对象、删除如何传播以及可空字段如何解释,变更时由契约测试提醒调用者而不是依赖隐式约定。

3.5 建立映射

从最小字段映射开始,逐步处理集合、继承和多态;每增加一层都保留替代方案。建立映射的过程应先锁定身份,再补集合和继承,最后处理写回与并发;每一步都要能用一个失败样本证明新增机制解决了真实问题。

3.6 使用元数据

让字段、可空性和关系元数据可检查、可版本化,避免映射错误只在运行时暴露。使用元数据不等于把所有规则藏进装饰器;字段类型、外键方向、默认值和版本都应能在启动检查或契约测试中被看见。

3.7 数据库连接

明确连接创建、事务范围、释放和失败重试的所有权,不把连接泄漏到领域对象。数据库连接的证据包括创建者、释放者、事务边界和超时策略;连接失败必须与合法空结果区分,重试也不能把一个事务悄悄提交两次。

3.8 其他问题

复核并发、批量、缓存、分页和数据库方言对对象语义的影响。其他问题不是附录垃圾桶,而是检查缓存失效、批量写回、分页顺序和方言差异是否改变身份与一致性;每项都应有接受条件和回滚路径。

3.9 进一步阅读

把本章证据卡与各个具体映射模式连接起来,再进入单模式章节做局部决策。进一步阅读应沿对象身份、读取形状、写回事务和连接边界选择下一个模式,而不是把章节总结当成默认实现;局部决策必须回链到本章的阻抗失配证据。

专属设计案例:订单聚合持久化

把订单聚合持久化切成“对象图 → 身份关系 → 读取 → 变更跟踪 → 写回”五个观察点。第3章映射到关系数据库的设计草案必须写出谁拥有状态、谁作出业务决定、失败怎样传播,以及身份、工作单元、关系映射、继承映射、连接边界中哪个指标最先提示当前方案不再适用。

设计记录采用五个可换行字段:单元键为 poeaa24-chapter-03-relational-mapping;模式族为 mapping;裁决是“能解释映射到关系数据库的边界与选择轴,逐项覆盖 9 个目录节点,并在同一应用切片中验证”;观测项包括身份、工作单元、关系映射、继承映射、连接边界;拒绝条件是“对象行为、连接生命周期或事务责任无法被独立测试”。

配置不是生产框架语法,而是一张评审卡。对第3章映射到关系数据库的任何实现都要能把运行证据重新映射到这张卡;如果更换 ORM、Web 框架或部署平台后无法回答同一组问题,说明决定依赖的是工具偶然行为而不是映射语义。

阻抗失配解剖

对象模型有继承、关联、多态和行为封装;关系模型以行、列、主键和外键表达结构。

阻抗失配:对象世界 vs 关系世界对象世界Order- items: List+ total(): MoneyCustomer- orders: Set+ credit()1..*Payment«abstract»CashCard· 继承 + 多态· 对象图(关联网络)· 行为封装在对象内· 身份由引用表达关系世界ordersid | cust_idtotal | statuscreated_atcustomersid | namecredit_limitFK· 只有行和列(扁平)· 关系靠外键(退化为 ID)· 没有继承(只有 JOIN)· 数据与行为完全分离· 身份由主键表达五个冲突点① 继承无对应 ② 关联退化为外键 ③ 多态无法表达④ 行为无处安放 ⑤ 粒度不匹配(对象图 vs 行集)映射策略选择轴(复杂度递增)Active Record对象=行Row Data Gateway网关隔离 SQLData Mapper独立映射层+ UoW + IdMap完整对象图管理映射越复杂,对象模型越自由——代价是间接层和维护成本
对象模型有继承、关联、多态和行为封装;关系模型只有行、列和外键。 两者之间的鸿沟就是「阻抗失配」。映射策略从 Active Record 到 Data Mapper + Unit of Work, 复杂度递增,对象模型的自由度也递增。

这张图把五个冲突点和从主动记录到数据映射器的复杂度轴放在同一张图上,帮助学习者在增加映射机制前先定位真实差异。

映射策略:先看对象行为,再增加间接层简单查询投影 / 记录集无独立行为与写回单表行为主动记录 / 表数据入口行为与一张表紧密对应复杂聚合Data Mapper + Unit of Work身份、事务与对象图独立演化拒绝条件:没有真实变化轴,却为“未来灵活性”增加完整映射层复杂度应由身份、行为和事务证据推动,而不是由 ORM 功能表推动
从投影到主动记录,再到数据映射器与工作单元,映射复杂度随对象行为和事务需求递增。

第二张图把同一选择轴转成决策顺序:先确认是否有对象行为,再判断身份、事务和对象图是否足以支持更复杂的映射器。

选择与拒绝矩阵

评审问题选择本章映射策略的证据应拒绝或改用其他方案的信号
责任对象图到写回的所有者清晰领域对象直接持有数据库连接
变化表结构与对象行为可以独立演化每次改字段都要修改业务行为
失败映射错误、并发冲突和连接失败可分类观测所有错误都变成空对象或空集合
替代已比较主动记录、投影和数据映射器只因 ORM 默认配置就跳过设计评审

代码实践:最小数据映射器与工作单元

订单对象和关系行使用不同的类型,OrderMapper 负责转换,UnitOfWork 负责一次提交。必须保持身份和可空字段语义,不能在转换时静默丢失信息。

type Order = {
  id: string;
  status: "open" | "paid";
  totalCents: number;
};
 
type OrderRow = {
  orderId: string;
  status: "open" | "paid";
  totalCents: number;
};
 
interface OrderStore {
  find(orderId: string): OrderRow | undefined;
  upsert(row: OrderRow): void;
}
 
class OrderMapper {
  constructor(private readonly store: OrderStore) {}
 
  load(orderId: string): Order | undefined {
    const row = this.store.find(orderId);
    return row
      ? { id: row.orderId, status: row.status, totalCents: row.totalCents }
      : undefined;
  }
 
  toRow(order: Order): OrderRow {
    return {
      orderId: order.id,
      status: order.status,
      totalCents: order.totalCents,
    };
  }
}
 
class UnitOfWork {
  private readonly dirty: Order[] = [];
 
  registerDirty(order: Order): void {
    this.dirty.push(order);
  }
 
  commit(mapper: OrderMapper, store: OrderStore): void {
    for (const order of this.dirty) store.upsert(mapper.toRow(order));
    this.dirty.length = 0;
  }
}

这段代码把对象、行、映射和提交分开,测试可以用内存 OrderStore 验证转换与回写。生产实现还应把 commit 放入数据库事务,并补充身份复用、并发冲突、连接释放和部分失败的测试。

常见误区

可验证练习

练习

本组练习覆盖 3.1 架构模式、3.2 行为问题、3.3 读取数据、3.4 结构映射模式、3.5 建立映射、3.6 使用元数据、3.7 数据库连接、3.8 其他问题和 3.9 进一步阅读。

问题 1:识别失配。 OrderRow.total_cents 被直接当作 Money 对象使用,可能丢失什么信息?

问题 2:检查工作单元。 一个订单的主表写入成功、明细表写入失败,应该让调用者看到什么?

问题 3:选择策略。 一个只读报表需要跨表汇总但不承载业务行为,应构造完整对象图吗?

名词解释

名词解释

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

阻抗失配

对象模型的身份、关联、继承和行为与关系模型的行、列、外键之间无法直接对应的结构差异。

工作单元

把一组对象变更登记、协调并在一个事务中提交或回滚的边界协作者。

关系映射

把关系行转换成对象并把对象变更写回关系表的边界组件。

本章小结

掌握第3章映射到关系数据库的标志不是记住定义,而是能在订单聚合持久化中解释“对象图 → 身份关系 → 读取 → 变更跟踪 → 写回”的责任链,利用身份、工作单元、关系映射、继承映射、连接边界作出可证伪的选择,并在对象行为、连接生命周期或事务责任无法测试时明确拒绝当前方案。

前后导航

来源与改写范围

资料与写作方式声明

本章以Martin Fowler《企业应用架构模式》与公开模式目录权威目录界定学习范围,并结合正文列出的技术资料独立重写;不宣称复现原书正文,也不沿用原作表述。

原作版权归作者与出版社所有;本站原创教学结构与表述仅供学习交流。

讨论

评论区加载中…