Spring Boot 确认发票后如何更新支付方式实体?
结论
问题通常不在 @Transactional 或 saveAll(),而是查询参数传错了。
findByTempInvoiceEntity_TempInvoiceId(...) 接收的是临时发票 ID,原代码传入的却是:
payment.getPaymentId()
支付记录 ID 和临时发票 ID 通常不是同一个值,所以查询结果很可能为空。查不到 CardEntity,也就无法更新 confirmInvoiceId。
另外还有三个问题需要处理:
- 删除
TempInvoiceEntity时,CardEntity可能仍在引用它,从而触发外键约束或级联操作。 - 第二种写法在
filter()中更新数据,而且始终返回false。这种副作用用法不合适。 - 方法捕获了所有异常,却没有重新抛出。这可能导致事务不按预期回滚,也会掩盖真正的数据库错误。
先确认查询维度
当前 Repository 方法是:
List<CardEntity> findByTempInvoiceEntity_TempInvoiceId(Long tempInvoiceId);
它会按照下面的实体路径查询:
CardEntity.tempInvoiceEntity.tempInvoiceId
因此,这里应该传入 invoiceId:
List<CardEntity> cardEntities =
cardRepo.findByTempInvoiceEntity_TempInvoiceId(invoiceId);
而不是:
payment.getPaymentId()
如果实际业务关系是“一条支付记录对应一条或多条卡支付记录”,Repository 就应该按 paymentId 查询:
List<CardEntity> findByPaymentId(Long paymentId);
调用方式也要相应改为:
List<CardEntity> cardEntities =
cardRepo.findByPaymentId(payment.getPaymentId());
这两种查询对应不同的业务关系,不能混用。
推荐实现
如果所有卡支付记录都直接关联临时发票,就不必针对每个 PaymentsEntity 重复查询。按发票一次查出并更新即可:
@Repository
public interface CardRepo extends JpaRepository<CardEntity, Long> {
List<CardEntity> findByTempInvoiceEntity_TempInvoiceId(Long tempInvoiceId);
}
服务方法:
private void alterPaymentMethodConfirmInvoice(
Long tempInvoiceId,
List<PaymentsEntity> payments,
ConfirmInvoiceEntity confirmedInvoice) {
boolean containsCardPayment = payments.stream()
.anyMatch(payment ->
"card".equalsIgnoreCase(payment.getPaymentType()));
if (!containsCardPayment) {
return;
}
List<CardEntity> cardEntities =
cardRepo.findByTempInvoiceEntity_TempInvoiceId(tempInvoiceId);
for (CardEntity cardEntity : cardEntities) {
cardEntity.setConfirmInvoiceEntity(confirmedInvoice);
// 如果确认后要删除临时发票,必须解除这个外键关系。
cardEntity.setTempInvoiceEntity(null);
}
cardRepo.saveAll(cardEntities);
}
主流程需要明确传入原始的 invoiceId:
@Override
@Transactional
public NonPaginatedResponse insertIntoConfirmInvoice(Long invoiceId) {
TempInvoiceEntity selectedTempInvoice = tempInvoiceRepo.findById(invoiceId)
.orElseThrow(() ->
new IllegalArgumentException("Temp invoice not found: " + invoiceId));
ConfirmInvoiceEntity confirmedInvoice =
createNewConfirmInvoiceData(selectedTempInvoice);
boolean cartItemsConfirmed =
confirmProductCartService.confirmTheCartItems(invoiceId, confirmedInvoice);
if (!cartItemsConfirmed) {
throw new IllegalStateException("Cart items could not be confirmed");
}
List<PaymentsEntity> payments =
paymentsRepo.findBySalesInvoice_TempInvoiceId(invoiceId);
alterPaymentMethodConfirmInvoice(
invoiceId,
payments,
confirmedInvoice
);
List<PaymentsEntity> confirmedPayments = payments.stream()
.map(payment -> {
PaymentsEntity confirmedPayment = new PaymentsEntity();
confirmedPayment.setPaymentType(payment.getPaymentType());
confirmedPayment.setPaidDate(payment.getPaidDate());
confirmedPayment.setPaidAmount(payment.getPaidAmount());
confirmedPayment.setConfirmInvoice(confirmedInvoice);
confirmedPayment.setSalesInvoice(null);
return confirmedPayment;
})
.toList();
paymentsRepo.saveAll(confirmedPayments);
/*
* 在删除临时发票之前提前 flush,可以让外键、字段映射等错误
* 在事务方法内部暴露出来。
*/
cardRepo.flush();
paymentsRepo.flush();
tempInvoiceRepo.delete(selectedTempInvoice);
NonPaginatedResponse response = new NonPaginatedResponse();
response.setStatus(HttpStatus.ACCEPTED);
response.setSuccessMessage("Invoice has been confirmed.");
response.setResult(null);
return response;
}
不要用 filter() 执行更新
下面的代码确实会在调用 .toList() 时执行,但它的语义不对:
selectAllPayments.stream().filter(allPay -> {
// 修改数据库
return false;
}).toList();
filter() 应该只用来筛选元素。这里始终返回 false,得到的列表一定为空,而且把数据库更新藏在筛选操作中,也不便于阅读和排查。
如果只是遍历,使用普通循环即可:
for (PaymentsEntity payment : selectAllPayments) {
if ("card".equalsIgnoreCase(payment.getPaymentType())) {
// 更新卡支付记录
}
}
也可以先判断是否存在卡支付:
boolean hasCardPayment = selectAllPayments.stream()
.anyMatch(payment ->
"card".equalsIgnoreCase(payment.getPaymentType()));
检查关联字段是否由 CardEntity 维护
按照当前映射,@JoinColumn 写在 CardEntity 一侧,所以它是关联的拥有方:
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "confirmInvoiceId")
private ConfirmInvoiceEntity confirmInvoiceEntity;
原则上,下面的赋值就可以更新 card_tbl.confirmInvoiceId:
cardEntity.setConfirmInvoiceEntity(confirmedInvoice);
同一事务中查询出来的 CardEntity 通常是托管实体。即使不调用 saveAll(),JPA 也会在事务提交时通过脏检查执行更新。当然,保留 saveAll() 也没有问题。
还要核对数据库列名和相关约束:
@JoinColumn(name = "confirmInvoiceId")
这里的 confirmInvoiceId 必须确实是 card_tbl 中的外键列。如果数据库实际使用 confirm_invoice_id,映射应改成:
@JoinColumn(name = "confirm_invoice_id")
private ConfirmInvoiceEntity confirmInvoiceEntity;
同时检查这个列是否被其他属性重复映射成只读字段,例如:
@Column(name = "confirmInvoiceId", insertable = false, updatable = false)
如果存在重复映射,或者配置了 updatable = false,Hibernate 就不会按预期更新该列。
删除临时发票时注意外键
原流程最后执行:
tempInvoiceRepo.deleteById(invoiceId);
但 CardEntity 中可能仍然保留着这个关联:
private TempInvoiceEntity tempInvoiceEntity;
如果 card_tbl.tempInvoiceId 还指向即将删除的临时发票,数据库可能拒绝删除。某些级联配置也可能导致卡支付记录被删除。
确认发票时,通常需要同时迁移关联:
cardEntity.setConfirmInvoiceEntity(confirmedInvoice);
cardEntity.setTempInvoiceEntity(null);
对应的数据库列必须允许 NULL:
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "tempInvoiceId", nullable = true)
private TempInvoiceEntity tempInvoiceEntity;
如果业务需要保留临时发票的历史记录,就不应删除 TempInvoiceEntity,而应增加状态字段,例如 CONFIRMED。
不要吞掉事务异常
原代码捕获所有异常后直接返回:
catch (Exception e) {
e.printStackTrace();
// 返回错误响应
}
Spring 默认会在异常从 @Transactional 方法抛出时自动回滚。异常一旦被捕获,事务可能继续提交之前已经执行的部分操作。
更合适的做法是让异常继续向外传播,再由统一异常处理器生成响应:
@Override
@Transactional
public NonPaginatedResponse insertIntoConfirmInvoice(Long invoiceId) {
// 业务逻辑
}
如果必须在当前方法中捕获异常,就应该重新抛出:
catch (Exception e) {
throw new IllegalStateException(
"Couldn't confirm invoice: " + invoiceId, e);
}
也可以显式标记事务回滚,但通常没有重新抛出清晰:
TransactionAspectSupport.currentTransactionStatus()
.setRollbackOnly();
排查时最先检查的内容
先确认查询是否真的返回了数据:
List<CardEntity> cards =
cardRepo.findByTempInvoiceEntity_TempInvoiceId(invoiceId);
if (cards.isEmpty()) {
throw new IllegalStateException(
"No card records found for temp invoice: " + invoiceId);
}
如果列表为空,问题出在查询参数或已有数据,而不是 setter。
如果列表不为空,但数据库仍未更新,可以按以下顺序检查:
@JoinColumn是否指向card_tbl中正确的列。setConfirmInvoiceEntity()是否确实给字段赋了值。- 该关联是否配置了
updatable = false。 - 删除临时发票时是否出现外键或级联异常。
- 异常是否被
catch (Exception)隐藏。 - 是否在事务提交前查看数据库,因而误以为更新没有发生。
需要修正的重点是明确区分 paymentId 和 tempInvoiceId,根据真实的关联关系查询 CardEntity,更新关联的拥有方,并在删除临时发票前解除旧关联。
备注:内容仅供参考。
