You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.07 19:55:31