10.4 数据映射器

以独立映射层在对象和数据库之间搬运数据,使领域对象、数据库模式和映射器彼此解耦。

学习目标

  • 能画出订单从领域对象到数据库行、再从数据库行回到对象的映射边界,并指出每一步的责任者
  • 能用映射契约、对象身份和变更集解释数据映射器如何保持读写一致,而不把数据库细节带进领域对象
  • 能根据关联复杂度、查询成本和失败语义选择数据映射器,并用测试验证加载、保存与并发冲突

为什么 10.4 数据映射器 值得单独学习

是一种把对象与数据源之间的转换放在独立层中的模式:领域对象不需要知道表名、列名或连接细节,映射器负责把查询结果构造成对象,也负责把对象变化写回数据源。它解决的不是“如何调用 ORM”,而是“谁拥有转换责任以及何时允许状态离开边界”。

在订单聚合持久化中,订单可能包含客户、订单项和状态行为,而数据库可能把它拆成多张表。若让领域对象直接拼接 SQL,表结构变化会沿调用链扩散;若让通用转换器无条件加载所有关联,查询次数和内存成本又会失控。数据映射器把这两个方向的变化集中在可审查的转换层,同时保留领域对象的业务行为。

本章的订单案例、映射实验、练习和判断标准均为课程原创;公开图书页与模式目录只用于核对 10.4 的模式名称、模式族和公开范围。这里不复现原书正文、插图或代码,而是用新的观察点验证模式是否成立。

先建立直觉:一条记录怎样成为一个对象

先预测:orders 表只有 idcustomer_idstatusamount_cents,但订单对象还需要金额值对象和订单项集合。哪些转换应该留在映射器,哪些行为必须留在领域对象?一个可靠的分界是:结构转换交给映射器,业务决定交给对象或领域服务。

应表达订单状态、金额规则和可执行的业务行为,而不是暴露数据库列。映射器可以读取一行或一组行,组装领域对象,再把对象的持久化状态拆回目标列;它不应因为方便就调用 approve()、决定折扣,或把数据库异常伪装成业务成功。

独立层还需要一个可测试的:列的类型、空值、默认值、主键、关联加载范围和写回条件都必须有明确约定。契约变化时,应先更新映射测试,再评估领域对象是否真的需要变化。

目录单元到教学证据

10.4 数据映射器

10.4 数据映射器 的学习边界里,数据映射器不是待背诵的目录词,而是用来检查“领域调用”是否把结构转换责任交给正确对象。对订单聚合持久化,学习者要记录对象身份、关联加载和写回结果的可观察变化,并说明这些证据何时支持或否定数据映射器。

单元键为 poeaa24-pattern-08-data-mapper;模式族为 mapping;本章的裁决是“能在不修改领域对象的情况下替换存储结构,并用映射测试证明身份、关系和更新正确”。观测项包括对象身份、关联查询次数、映射成本、版本条件和测试替身;拒绝条件是“映射器开始拥有业务规则,或加载/写回范围无法被测试固定”。

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

把一次 loadOrder(id) 切成“查询订单行 → 查询订单项 → 组装对象 → 执行业务行为 → 拆分写回”五个观察点。映射器拥有行与对象之间的翻译,订单对象拥有状态变化,应用服务拥有事务与用例顺序;三者的职责不能靠某个框架默认行为猜测。

在第一次加载时,映射器根据订单 ID 建立,并决定同一工作单元里重复请求相同 ID 是否复用同一个实例。身份复用可以避免两个副本互相覆盖,但也会带来缓存失效、内存增长和跨请求泄漏的风险,所以必须明确范围,而不能把全局缓存当成默认答案。

保存时,映射器只处理结构变化和持久化条件。领域对象产生的应经过验证,再由映射器转换成订单表和订单项表的更新;如果版本条件不匹配,映射器报告冲突,应用流程决定重载、合并还是放弃。这样,数据库的失败语义不会被隐藏在对象的属性访问中。

模式结构图

Data Mapper:对象与表完全解耦Order(领域对象)- items: OrderItem[]- customer: Customer+ calculateTotal()不知道 DB 存在OrderMapperfind(id)insert(obj)update(obj)delete(obj)orders 表id | customer_id | amountstatus | created_at纯关系结构Mapper 双向翻译:行 ↔ 对象核心优势:• 领域对象可独立测试(无需 DB) • 表结构变化不影响领域层 • 一个对象可映射到多张表适用:领域复杂、需要测试隔离、表结构与对象结构差异大
Data Mapper 在领域对象和数据库之间建立翻译层。 领域对象完全不知道数据库存在,Mapper 负责双向转换。

图中三段边界分别表示领域对象、独立映射器和关系结构。读路径是“行 → 对象”,写路径是“对象 → 行”;两条路径都应经过同一份映射契约,而不是让领域对象偶然依赖列名。

用订单映射走一遍读写生命周期

分步1 / 3

先划出对象与行的边界

先预测:如果订单对象新增一个金额值对象,而数据库仍以整数分为单位保存,哪一层负责把两种表示互相转换?先写出领域字段、数据库字段和主键,再把结构转换集中到 OrderMapper,检查订单对象是否完全不知道表名与连接。

Data Mapper:对象与表完全解耦Order(领域对象)- items: OrderItem[]- customer: Customer+ calculateTotal()不知道 DB 存在OrderMapperfind(id)insert(obj)update(obj)delete(obj)orders 表id | customer_id | amountstatus | created_at纯关系结构Mapper 双向翻译:行 ↔ 对象核心优势:• 领域对象可独立测试(无需 DB) • 表结构变化不影响领域层 • 一个对象可映射到多张表适用:领域复杂、需要测试隔离、表结构与对象结构差异大
Data Mapper 在领域对象和数据库之间建立翻译层。 领域对象完全不知道数据库存在,Mapper 负责双向转换。

常见误区

映射契约与可替换实现

下面的伪代码展示边界,而不是某个 ORM 的生产 API:

type OrderRow = {
  id: string;
  customer_id: string;
  status: string;
  amount_cents: number;
  version: number;
};
 
type Order = {
  id: string;
  customerId: string;
  status: "draft" | "approved";
  amountCents: number;
};
 
class OrderMapper {
  toDomain(row: OrderRow): Order {
    return {
      id: row.id,
      customerId: row.customer_id,
      status: row.status as Order["status"],
      amountCents: row.amount_cents,
    };
  }
 
  toRow(
    order: Order,
    version: number,
  ): Pick<OrderRow, "id" | "status" | "amount_cents" | "version"> {
    return {
      id: order.id,
      status: order.status,
      amount_cents: order.amountCents,
      version,
    };
  }
}

这段代码刻意没有把 approve() 放进映射器,也没有把数据库连接暴露给 Order。生产实现还要决定关联的批量查询、事务隔离、删除语义和版本冲突,但这些决定都可以沿映射契约逐项测试。

选择与拒绝矩阵

评审问题选择数据映射器的证据应拒绝或准备迁移的信号
责任领域对象与表结构分别演化,翻译责任集中领域对象直接依赖列名、连接或 ORM 生命周期
关联加载范围和查询次数有明确测试每次读取都隐式加载完整对象图
身份工作单元内同一主键的实例策略可解释全局缓存或多个副本导致旧快照覆盖新状态
写回变更集、版本条件和失败结果可观察映射器直接执行业务规则或吞掉数据库异常
替代领域复杂且对象/表结构差异值得独立层对象与表天然同构、映射规则少到不值得维护

本章小结

掌握 10.4 数据映射器的标志,不是记住“对象和表之间有一层代码”,而是能说明领域对象、映射器和应用服务各自拥有的责任。读路径要验证类型、空值、关联与对象身份,写路径要验证变更集、事务边界、版本冲突和失败传播;只有这些证据可测试,独立映射层才值得承担额外间接成本。

本章练习

练习

问题 1: 订单对象的 amountCents 需要改成金额值对象,但数据库列暂时不变。应该修改领域对象、映射器,还是两者都修改?请说明测试重点。

问题 2: 同一工作单元两次加载订单返回两个实例,第二个实例保存后覆盖了第一个实例刚完成的状态变化。这个问题属于查询、对象身份还是写回契约?如何修复?

问题 3: 映射器为了“方便”在加载订单时调用 approve(),并把库存服务失败转换成空订单返回。应如何拆分?

前后导航

出处声明

名词解释

名词解释

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

数据映射器

位于领域对象与数据源之间、负责双向结构转换和持久化结果翻译的独立层。

领域对象

承载业务状态、规则和行为的对象,不依赖数据库表、列或连接细节。

映射契约

规定字段类型、空值、主键、关联范围、版本条件和读写转换结果的可测试约定。

对象身份

在一个明确生命周期内,同一持久化标识如何对应内存对象实例的策略。

变更集

领域对象经过业务验证后,需要由映射器转换并写回数据源的状态变化集合。

讨论

评论区加载中…