DDD中从存储恢复领域对象的最佳实践:PO转DO私有字段处理
DDD中领域对象(DO)与持久化对象(PO)转换的最佳实践(基于MyBatis)
针对你遇到的从PO恢复DO时无法设置私有字段、又不想触发领域事件的问题,结合MyBatis技术栈,以下是几种落地性强的最佳实践:
方案一:给领域对象添加包级私有全参构造函数
这是最符合DDD封装原则的方案——因为Repository通常和领域对象处于同一个领域包下,包级私有构造函数仅允许Repository调用,外部业务代码无法访问,避免滥用。
修改领域对象
@Getter public class Order extends AggregateRoot<Order> { // 原有字段保持不变 // 包级私有全参构造函数,仅用于Repository恢复对象状态 Order(Long id, String orderSn, Long userId, BigDecimal totalAmount, BigDecimal paidAmount, OrderStatus status, String address, Date orderTime, List<OrderLine> lines) { this.id = id; this.orderSn = orderSn; this.userId = userId; this.totalAmount = totalAmount; this.paidAmount = paidAmount; this.status = status; this.address = address; this.orderTime = orderTime; this.lines = lines; } // 原有领域方法不变 public void payOrder() { this.status = OrderStatus.PAID; publish(new OrderPaidEvent()); } }
Repository实现
public Order findById(long id) { DemoOrderPo orderPo = demoOrderMapper.selectById(id); Wrapper<DemoOrderLinePo> query = Wrappers.<DemoOrderLinePo>lambdaQuery() .eq(DemoOrderLinePo::getOrderId, id); List<DemoOrderLinePo> orderLinePoList = demoOrderLineMapper.selectList(query); // 先转换OrderLinePo到OrderLine(同理给OrderLine加包级私有构造函数) List<OrderLine> lines = orderLinePoList.stream() .map(po -> new OrderLine(po.getId(), po.getProductId(), po.getQuantity(), po.getPrice())) .collect(Collectors.toList()); // 直接调用包级私有构造函数创建完整的Order对象 return new Order( orderPo.getId(), orderPo.getOrderSn(), orderPo.getUserId(), orderPo.getTotalAmount(), orderPo.getPaidAmount(), orderPo.getStatus(), orderPo.getAddress(), orderPo.getOrderTime(), lines ); }
优点:严格遵循DDD封装,避免外部随意修改领域对象状态;缺点:字段变更时需同步更新构造函数参数,维护成本略高。
方案二:利用MyBatis ResultMap直接映射到DO
MyBatis本身支持通过反射绕过Java访问权限限制,直接将数据库查询结果映射到DO的私有字段,无需手动转换PO和DO,是最省心的方案。
定义Mapper XML的ResultMap
<resultMap id="OrderResultMap" type="com.yourdomain.order.Order"> <id column="id" property="id"/> <result column="order_sn" property="orderSn"/> <result column="user_id" property="userId"/> <result column="total_amount" property="totalAmount"/> <result column="paid_amount" property="paidAmount"/> <!-- 枚举类型需指定类型转换器 --> <result column="status" property="status" typeHandler="com.yourdomain.handler.OrderStatusTypeHandler"/> <result column="address" property="address"/> <result column="order_time" property="orderTime"/> <!-- 关联映射OrderLine列表 --> <collection property="lines" ofType="com.yourdomain.order.OrderLine"> <id column="line_id" property="id"/> <result column="product_id" property="productId"/> <result column="quantity" property="quantity"/> <result column="price" property="price"/> </collection> </resultMap> <select id="selectOrderWithLines" resultMap="OrderResultMap"> SELECT o.id, o.order_sn, o.user_id, o.total_amount, o.paid_amount, o.status, o.address, o.order_time, ol.id as line_id, ol.product_id, ol.quantity, ol.price FROM demo_order o LEFT JOIN demo_order_line ol ON o.id = ol.order_id WHERE o.id = #{id} </select>
Repository实现
public Order findById(long id) { // 直接调用Mapper获取完整的Order对象,MyBatis自动完成映射 return demoOrderMapper.selectOrderWithLines(id); }
优点:无需手动编写转换逻辑,开发效率高;缺点:字段或表结构变更时需同步更新ResultMap,需维护映射关系。
方案三:使用反射工具类(限定仅在Repository中使用)
如果不想修改领域对象的封装,也可以用Spring BeanUtils、Apache BeanUtils等反射工具,直接将PO的字段值复制到DO的私有字段。注意:仅允许在Repository的对象恢复逻辑中使用,禁止在业务代码中滥用。
Repository实现
public Order findById(long id) { DemoOrderPo orderPo = demoOrderMapper.selectById(id); Wrapper<DemoOrderLinePo> query = Wrappers.<DemoOrderLinePo>lambdaQuery() .eq(DemoOrderLinePo::getOrderId, id); List<DemoOrderLinePo> orderLinePoList = demoOrderLineMapper.selectList(query); Order order = new Order(); // 复制PO字段到DO,忽略null值可添加参数:BeanUtils.copyProperties(orderPo, order, "lines"); BeanUtils.copyProperties(orderPo, order); // 转换OrderLine并设置到Order(需给Order添加包级私有setLines方法) List<OrderLine> lines = orderLinePoList.stream() .map(po -> { OrderLine line = new OrderLine(); BeanUtils.copyProperties(po, line); return line; }) .collect(Collectors.toList()); order.setLines(lines); // 该方法为包级私有,仅Repository可调用 return order; }
优点:无需修改领域对象结构;缺点:反射存在微小性能损耗,字段名不一致时需手动处理,需严格控制使用范围。
方案四:定义包级私有持久化接口
通过领域包内的私有接口,明确区分业务方法和持久化状态设置方法,进一步强化封装性。
定义持久化接口(领域包内可见)
// 仅在领域包内可见的接口,用于Repository恢复对象状态 interface PersistableOrder { void setId(Long id); void setOrderSn(String orderSn); void setUserId(Long userId); void setTotalAmount(BigDecimal totalAmount); void setPaidAmount(BigDecimal paidAmount); void setStatus(OrderStatus status); void setAddress(String address); void setOrderTime(Date orderTime); void setLines(List<OrderLine> lines); }
修改领域对象实现接口
@Getter public class Order extends AggregateRoot<Order> implements PersistableOrder { // 原有字段不变 // 实现接口的方法,均为包级私有访问权限 @Override void setId(Long id) { this.id = id; } @Override void setOrderSn(String orderSn) { this.orderSn = orderSn; } // 其他set方法实现,均不触发任何领域事件 // 原有领域方法不变 public void payOrder() { this.status = OrderStatus.PAID; publish(new OrderPaidEvent()); } }
Repository实现
public Order findById(long id) { DemoOrderPo orderPo = demoOrderMapper.selectById(id); Wrapper<DemoOrderLinePo> query = Wrappers.<DemoOrderLinePo>lambdaQuery() .eq(DemoOrderLinePo::getOrderId, id); List<DemoOrderLinePo> orderLinePoList = demoOrderLineMapper.selectList(query); Order order = new Order(); PersistableOrder persistable = (PersistableOrder) order; // 通过接口设置所有字段 persistable.setId(orderPo.getId()); persistable.setOrderSn(orderPo.getOrderSn()); persistable.setUserId(orderPo.getUserId()); persistable.setTotalAmount(orderPo.getTotalAmount()); persistable.setPaidAmount(orderPo.getPaidAmount()); persistable.setStatus(orderPo.getStatus()); persistable.setAddress(orderPo.getAddress()); persistable.setOrderTime(orderPo.getOrderTime()); // 转换OrderLine并设置 List<OrderLine> lines = orderLinePoList.stream() .map(po -> { OrderLine line = new OrderLine(); PersistableOrderLine linePersistable = (PersistableOrderLine) line; linePersistable.setId(po.getId()); linePersistable.setProductId(po.getProductId()); linePersistable.setQuantity(po.getQuantity()); linePersistable.setPrice(po.getPrice()); return line; }) .collect(Collectors.toList()); persistable.setLines(lines); return order; }
优点:封装性极强,明确区分业务和持久化操作;缺点:需要额外定义接口,代码量增加。
方案选择建议
- 追求严格DDD封装:优先选方案一或方案四;
- 追求开发效率:优先选方案二(利用MyBatis原生特性);
- 临时过渡或不想修改领域对象:选方案三(但需严格控制使用范围)。
内容的提问来源于stack exchange,提问作者Kratos
相关产品推荐
相关产品推荐

