Spring Boot Controller多次调用Save()报父键不存在异常排查
问题场景
Spring Boot项目开发事件详情新增接口时,在标注了@Transactional、@PostMapping("/trans/incidentdetails")的Controller方法updateIncidentCost中实现多表关联数据保存逻辑,执行时抛出ORA-02291 完整性约束(OCCAL.FK_TRX_INCIDENT_DETAILS_1)违反 - 父键不存在错误,触发DataIntegrityViolationException数据完整性异常。
原逻辑执行流程:
- 根据传入的
incidentNumber查询判断事件是否已存在,不存在则先创建IncidentDetails主表实体赋值,调用incidentDetailsRepository.save()保存主表数据,获取持久化返回的主表实体对象 - 查询全量
LobMaster业务线配置,遍历匹配不同业务线,组装对应的IncidentOutage子表实体存入列表,同时组装IncidentOutageSum汇总实体并关联已保存的主表实体 - 先后调用
incidentOutageSumRepository.save()保存汇总数据、incidentOutageRepository.saveAll()批量保存子表列表
问题代码
@Transactional @PrePersist @PostMapping("/trans/incidentdetails") public ResponseEntity<IncidentCreationResponse> updateIncidentCost(@Valid @RequestBody IncidentCreation incidentCreation ) { IncidentOutageSum incidentOutageSum = new IncidentOutageSum(); System.out.println("incidentNumber: " + incidentCreation.getIncidentNumber()); System.out.println("incidentDate: " + incidentCreation.getIncidentDate()); System.out.println("coeed : "+incidentCreation.getSumOfCoed()); Long checkIncidentId = incidentDetailsRepository.getIncidentDetailsByIncidentNumber(incidentCreation.getIncidentNumber()); System.out.println(checkIncidentId + " : checkIncidentId"); String incidentDate = incidentCreation.incidentDate; LocalDate dtFmt = null; try{ dtFmt = LocalDate.parse(incidentDate, DateTimeFormatter.ofPattern("uuuu-M-d") .withResolverStyle(ResolverStyle.STRICT)); System.out.println("dtFmt : "+dtFmt); } catch (DateTimeParseException e) { IncidentCreationResponse customResponse = new IncidentCreationResponse(); customResponse.setResponseCode(String.valueOf(HttpStatus.OK.value())); customResponse.setMessage("Incorrect Date Format for Incident Date"); return new ResponseEntity<IncidentCreationResponse>(customResponse, HttpStatus.OK); } if (checkIncidentId == null) { System.out.println("IF"); IncidentDetails incidentDetails = new IncidentDetails(); incidentDetails.setIncidentNumber(incidentCreation.getIncidentNumber()); incidentDetails.setIncidentDate(java.sql.Date.valueOf(dtFmt)); IncidentDetails incidentDetails1 = incidentDetailsRepository.save(incidentDetails); List<LobMaster> lobMaster = (List<LobMaster>) lobMasterRepository.findAll(); List<IncidentOutage> objectList = new ArrayList<>(); for(int i =0 ; i<lobMaster.size() ; i++) { System.out.println(incidentDetails1.getId()); if(lobMaster.get(i).getLobName().toUpperCase().trim().toString().equals("PREPAID") ) { System.out.println("PREPAID"); IncidentOutage incidentOutage = new IncidentOutage(); incidentOutage.setIncidentDetails(incidentDetails1); incidentOutage.setLobMaster(lobMaster.get(i)); incidentOutage.setCodOrderDegrade(incidentCreation.getIncidentOutageData().prepaid.codOrderDegrade); incidentOutage.setCoedServiceCallDegraded(incidentCreation.getIncidentOutageData().prepaid.coedServiceDegrade); incidentOutage.setColTransactionDip(incidentCreation.getIncidentOutageData().prepaid.colTransactionDip); incidentOutage.setCoplTransactionDip(incidentCreation.getIncidentOutageData().prepaid.coplTransactionDip); System.out.println("incidentOutage : "+incidentOutage); objectList.add(incidentOutage); } else if(lobMaster.get(i).getLobName().toUpperCase().trim().toString().equals("POSTPAID") ) { System.out.println("POSTPAID"); IncidentOutage incidentOutage = new IncidentOutage(); incidentOutage.setIncidentDetails(incidentDetails1); incidentOutage.setLobMaster(lobMaster.get(i)); incidentOutage.setCodOrderDegrade(incidentCreation.getIncidentOutageData().postpaid.codOrderDegrade); incidentOutage.setCoedServiceCallDegraded(incidentCreation.getIncidentOutageData().postpaid.coedServiceDegrade); incidentOutage.setColTransactionDip(incidentCreation.getIncidentOutageData().postpaid.colTransactionDip); incidentOutage.setCoplTransactionDip(incidentCreation.getIncidentOutageData().postpaid.coplTransactionDip); System.out.println("incidentOutage : "+incidentOutage); objectList.add(incidentOutage); } else if(lobMaster.get(i).getLobName().toUpperCase().trim().toString().equals("BROADBAND") ) { System.out.println("BROADBAND"); IncidentOutage incidentOutage = new IncidentOutage(); incidentOutage.setIncidentDetails(incidentDetails1); incidentOutage.setLobMaster(lobMaster.get(i)); incidentOutage.setCodOrderDegrade(incidentCreation.getIncidentOutageData().broadband.codOrderDegrade); incidentOutage.setCoedServiceCallDegraded(incidentCreation.getIncidentOutageData().broadband.coedServiceDegrade); incidentOutage.setColTransactionDip(incidentCreation.getIncidentOutageData().broadband.colTransactionDip); incidentOutage.setCoplTransactionDip(incidentCreation.getIncidentOutageData().broadband.coplTransactionDip); System.out.println("incidentOutage : "+incidentOutage); objectList.add(incidentOutage); } else if(lobMaster.get(i).getLobName().toUpperCase().trim().toString().equals("BLACK") ) { System.out.println("BLACK"); IncidentOutage incidentOutage = new IncidentOutage(); incidentOutage.setIncidentDetails(incidentDetails1); incidentOutage.setLobMaster(lobMaster.get(i)); incidentOutage.setCodOrderDegrade(incidentCreation.getIncidentOutageData().black.codOrderDegrade); incidentOutage.setCoedServiceCallDegraded(incidentCreation.getIncidentOutageData().black.coedServiceDegrade); incidentOutage.setColTransactionDip(incidentCreation.getIncidentOutageData().black.colTransactionDip); incidentOutage.setCoplTransactionDip(incidentCreation.getIncidentOutageData().black.coplTransactionDip); System.out.println("incidentOutage : "+incidentOutage); objectList.add(incidentOutage); } else if(lobMaster.get(i).getLobName().toUpperCase().trim().toString().equals("B2B") ) { System.out.println("B2B"); IncidentOutage incidentOutage = new IncidentOutage(); incidentOutage.setIncidentDetails(incidentDetails1); incidentOutage.setLobMaster(lobMaster.get(i)); incidentOutage.setCodOrderDegrade(incidentCreation.getIncidentOutageData().b2B.codOrderDegrade); incidentOutage.setCoedServiceCallDegraded(incidentCreation.getIncidentOutageData().b2B.coedServiceDegrade); incidentOutage.setColTransactionDip(incidentCreation.getIncidentOutageData().b2B.colTransactionDip); incidentOutage.setCoplTransactionDip(incidentCreation.getIncidentOutageData().b2B.coplTransactionDip); System.out.println("incidentOutage : "+incidentOutage); objectList.add(incidentOutage); } else if(lobMaster.get(i).getLobName().toUpperCase().trim().toString().equals("DTH") ) { System.out.println("DTH"); IncidentOutage incidentOutage = new IncidentOutage(); incidentOutage.setIncidentDetails(incidentDetails1); incidentOutage.setLobMaster(lobMaster.get(i)); incidentOutage.setCodOrderDegrade(incidentCreation.getIncidentOutageData().dTH.codOrderDegrade); incidentOutage.setCoedServiceCallDegraded(incidentCreation.getIncidentOutageData().dTH.coedServiceDegrade); incidentOutage.setColTransactionDip(incidentCreation.getIncidentOutageData().dTH.colTransactionDip); incidentOutage.setCoplTransactionDip(incidentCreation.getIncidentOutageData().dTH.coplTransactionDip); System.out.println("incidentOutage : "+incidentOutage); objectList.add(incidentOutage); }else{ System.out.println("No matching Lob"); } } incidentOutageSum.setIncidentDetails(incidentDetails1); System.out.println("incidentCreation.sumOfCod: "+incidentCreation.getSumOfCod()); incidentOutageSum.setSumOfCod(incidentCreation.getSumOfCod()); incidentOutageSum.setSumOfCoed(incidentCreation.getSumOfCoed()); incidentOutageSum.setSumOfCopl(incidentCreation.getSumOfCopl()); incidentOutageSum.setSumOfCol(incidentCreation.getSumOfCol()); Float totalCost = incidentCreation.getSumOfCod() + incidentCreation.getSumOfCoed() + incidentCreation.getSumOfCopl() + incidentCreation.getSumOfCol(); IncidentCreationResponse customResponse = new IncidentCreationResponse(); customResponse.setResponseCode(String.valueOf(HttpStatus.CREATED.value())); customResponse.setTotalCost(totalCost); customResponse.setMessage("Record Saved Successfully"); IncidentOutageSum incidentOutageSum1 = incidentOutageSumRepository.save(incidentOutageSum); Iterable<IncidentOutage> incidentOutage = incidentOutageRepository.saveAll(objectList); return new ResponseEntity<IncidentCreationResponse>(customResponse, HttpStatus.CREATED); }else{ System.out.println("ELSE"); IncidentCreationResponse customResponse = new IncidentCreationResponse(); customResponse.setResponseCode(String.valueOf(HttpStatus.OK.value())); customResponse.setMessage("Record Already Exists"); return new ResponseEntity<IncidentCreationResponse>(customResponse, HttpStatus.OK); } }
报错日志
Hibernate: insert into trx_incident_details (incident_date, incident_number, modified_date, id) values (?, ?, ?, ?) Hibernate: insert into trx_outage_sum (incident_id, modified_date, sum_of_cod, sum_of_coed, sum_of_col, sum_of_copl, id) values (?, ?, ?, ?, ?, ?, ?) 2022-06-11 00:31:51.612 WARN 20637 --- [nio-8080-exec-7] o.h.engine.jdbc.spi.SqlExceptionHelper : SQL Error: 2291, SQLState: 23000 2022-06-11 00:31:51.612 ERROR 20637 --- [nio-8080-exec-7] o.h.engine.jdbc.spi.SqlExceptionHelper : ORA-02291: integrity constraint (OCCAL.FK_TRX_INCIDENT_DETAILS_1) violated - parent key not found 2022-06-11 00:31:51.612 INFO 20637 --- [nio-8080-exec-7] o.h.e.j.b.internal.AbstractBatchImpl : HHH000010: On release of batch it still contained JDBC statements 2022-06-11 00:31:51.618 WARN 20637 --- [nio-8080-exec-7] .m.m.a.ExceptionHandlerExceptionResolver : Resolved [org.springframework.dao.DataIntegrityViolationException: could not execute statement; SQL [n/a]; constraint [OCCAL.FK_TRX_INCIDENT_DETAILS_1]; nested exception is org.hibernate.exception.ConstraintViolationException: could not execute statement]
根因分析
- 注解使用错误:Controller接口方法上错误添加了
@PrePersist注解,该注解是JPA实体生命周期回调注解,仅能用于JPA实体类的内部方法,标注在Controller方法上会扰乱JPA持久化逻辑和Spring AOP代理逻辑。 - 外键映射配置错误:
IncidentOutageSum或IncidentOutage实体中关联IncidentDetails的@JoinColumn配置不正确,大概率是referencedColumnName没有指向主表主键id,导致插入从表时传入的外键值在主表中不存在。 - 事务配置不生效:
@Transactional标注在Controller方法上,多数Spring Boot项目默认事务拦截仅配置在Service层,Controller层方法不会被事务代理,会出现多个独立事务提交、主表插入未完成就执行从表插入的问题。 - 主键生成策略不匹配:Oracle12c之前的版本不支持IDENTITY自增主键,若
IncidentDetails主键配置为IDENTITY生成策略,会导致save返回的主键值和数据库实际插入值不一致,从表关联的外键值无效。
修复方案
- 删除Controller方法上的
@PrePersist注解,该注解不能用于Web接口方法。 - 调整主键生成策略适配Oracle,使用序列生成主表主键,参考配置:
@Id @Column(name = "id", nullable = false) @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "incident_details_seq") @SequenceGenerator(name = "incident_details_seq", sequenceName = "TRX_INCIDENT_DETAILS_SEQ", allocationSize = 1) private Long id;
- 检查所有关联实体的外键映射,确保
IncidentOutageSum、IncidentOutage中关联主表的字段配置正确,参考配置:
@ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "incident_id", referencedColumnName = "id", nullable = false) private IncidentDetails incidentDetails;
- 重构代码分层:将多表保存逻辑迁移到Service层,在Service层的public方法上添加
@Transactional注解,Controller层仅处理参数校验和请求响应转发,保证事务正常生效。 - 若仍存在SQL执行顺序问题,可在主表save操作后手动调用
incidentDetailsRepository.flush(),强制将主表数据刷入数据库后再执行后续从表保存操作。
内容的提问来源于stack exchange,提问作者ankush dhingan
相关产品推荐
相关产品推荐

