第4章 数据模型与领域模型分离:ORM映射策略、DTO/PO/DO转换、解耦规则
4.1 问题背景与核心概念
在分层架构中,数据持久化模型(PO/Persistence Object)与领域模型(DO/Domain Object)往往具有不同的生命周期与职责。直接复用同一模型会导致:
- 数据库结构变更直接污染领域逻辑
- ORM 注解/配置侵入领域类,破坏业务纯净性
- 序列化/反序列化约束影响对象设计(例如 JSON 字段与表字段耦合)
本章定义三种核心模型的边界与转换策略:
| 模型 | 全称 | 职责 | 归属层 |
|---|---|---|---|
| PO | Persistence Object | 直接映射数据库表(列、关联、主键) | 数据层(Repository 内部) |
| DO | Domain Object | 体现业务概念、行为、不变量 | 领域层(Domain 层) |
| DTO | Data Transfer Object | 跨层/跨进程传输,按需求裁剪字段 | 应用层/接口层 |
关键原则:依赖方向永远由外层指向内层,PO、DO、DTO 之间通过显式转换器(Converter)解耦,不允许跨层直接引用对方的内部模型。
4.2 边界与分层
数据层与领域层之间的边界由 Repository 接口 和 ORM 映射策略共同定义。Repository 接口返回 DO,隐藏 PO 的存在;ORM 框架负责将 PO 转化为 DO,或将 DO 持久化为 PO。典型的分层结构如下:
信任链路:领域层只信任 Repository 接口返回的 DO,不信任 PO;应用层只信任领域层,不直接操作 Repository 实现。Converter 是唯一被允许跨层转换的组件。
4.3 ORM 映射策略选型
主流策略包括全自动映射(JPA/Hibernate)、半自动映射(MyBatis)和手动映射(Spring JDBC Template)。选型权衡如下:
| 策略 | 映射方式 | 灵活度 | 学习成本 | 典型风险 |
|---|---|---|---|---|
| 全自动(JPA) | 注解/XML 定义关系,自动生成 SQL | 低 | 高 | N+1 查询、隐式延迟加载陷阱 |
| 半自动(MyBatis) | 手写 SQL + 结果映射 | 高 | 中 | SQL 分散、PO 与 DO 转换仍需手动 |
| 手动(JDBC Template) | 完全手写 SQL 与 RowMapper | 最高 | 低 | 重复代码多、易出错 |
风险提示:全自动映射如果未正确配置懒加载/急加载,会导致 N+1 查询性能问题。半自动映射容易导致 PO 与 DO 字段过度耦合(例如直接使用 PO 作为 DO)。手动映射虽然可控,但团队维护成本高。
4.4 DTO/PO/DO 转换落地步骤
- 定义模型接口契约
在领域层定义DO和Repository接口;在数据层定义PO和Repository实现;在应用层定义DTO。 - 选择转换方式
首选编译期工具(如 MapStruct)避免运行时反射开销;次选手动 Converte;避免对 DO 直接添加 getter/setter 以外的 ORM 注解。 - 定义转换接口
使用伪代码定义 Converter:
// 领域层:Converter 接口
public interface Converter<P, D> {
D toDomain(P po);
P toPO(D domain);
}
// 数据层实现:XXXConverter implements Converter<UserPO, UserDO> {
// 字段复制 + 规则转换
}
- 4. 隔离 P O 依赖:禁止任何领域层或应用层代码 import 数据层 PO 类。
- 5. 单元测试覆盖:验证所有字段映射正确性,尤其处理 null、嵌套对象、枚举。
4.5 验收标准
| 维度 | 验收条件 |
|---|---|
| 模型纯净度 | 领域层 DO 不含 @Entity、@Table 等 ORM 注解;PO 不含业务方法 |
| 转换独立性 | 转换器单元测试通过;字段映射覆盖率达到 100% |
| 循环依赖检测 | 编译期工具(如 ArchUnit)验证无跨层直接引用 PO |
| 性能 | 转换耗时占总请求时间 < 5%(基准测试) |
| 可维护性 | 新增字段只需修改 PO/DO 定义和 Converter,无需改动上层逻辑 |
落地最佳实践:在持续集成中加入 ArchUnit 规则,强制禁止 import 数据层 PO 到领域层;Converter 使用 MapStruct,自动生成映射代码,减少人工维护成本。
4.6 风险点与应对措施
- 模型膨胀:PO/DO/DTO 三者字段高度相似时,团队容易放弃分离。应对:坚持“一个模型一个职责”原则,通过代码审查保证。
- 延迟加载失效:在 Converter 中访问 PO 的延迟加载属性可能导致异常。应对:在 Repository 实现中限定时查询加载策略(JOIN FETCH)。
- 事务边界模糊:PO 的事务状态透传到领域层。应对:所有 PO 操作必须在数据层事务内完成,领域层只操作已加载的 DO。
通过上述策略,数据模型与领域模型的分离不再是理论口号,而是可度量、可验证的架构纪律。