订单是最典型的主表加明细结构:只保存主表成功、明细失败,会出现一个没有商品的空订单;反过来则会出现孤儿明细。项目把这笔业务动作封装到事务方法里。
一、事务方法
@Transactional(rollbackFor = Exception.class)
public void saveWithDetails(Order order, List<OrderDetail> details) {
save(order);
details.forEach(detail -> {
detail.setId(null);
detail.setOrderId(order.getOrderId());
detail.setOrderNo(order.getOrderNo());
});
orderDetailService.saveBatch(details);
}
Controller 负责 DTO 转换和调用,不自己拆分两次数据库写入。
二、rollbackFor 不能省
Spring 默认只对运行时异常回滚。加上 rollbackFor = Exception.class 后,受检异常同样触发回滚。业务代码将来引入受检异常时,事务语义不会被悄悄改变。
三、先补齐关联字段
明细的主键 ID 会被清空,避免调用方传入旧 ID 造成覆盖或冲突;订单 ID 和订单号统一由服务端补齐,保证明细一定挂在这张新订单上。
四、失败时向上抛
保存失败抛出 BusinessException,由全局异常处理转换成统一响应。事务边界内不要吞异常,否则方法看起来失败,数据库却已经提交了一半。
五、经验总结
事务应该围绕业务用例,而不是围绕 Repository 方法。订单保存这个动作天然要求主表和明细一致,所以边界就放在 saveWithDetails。