Spring JPA保存关联实体时如何防止数据库表出现重复条目
Spring JPA 新增城市时国家表重复插入问题修复方案
问题根因
同国家下新增城市时重复生成国家记录,核心是三个配置/逻辑缺陷:
CtyCity实体的@ManyToOne关联配置了cascade = CascadeType.ALL,每次保存城市时,只要关联的CntCountry对象无主键ID,JPA就会自动执行新国家的插入操作,不会判断该国家是否已存在COUNTRY表未对国家唯一标识字段加数据库唯一约束,底层无法拦截重复数据写入- 保存逻辑未做国家存在性校验,直接将前端传参转换的实体入库,完全没有复用已有国家记录的逻辑
- 额外风险:
CascadeType.ALL包含删除级联,后续删除城市时会连带删除关联的国家记录,属于明显的配置错误
分步修复方案
1. 给国家表加唯一约束,从数据库层防重
国家编码是稳定的全局唯一标识,适合作为去重判断依据,修改CntCountry实体类,给表加唯一约束:
@Getter @Setter @Entity @Table(name = "COUNTRY", uniqueConstraints = { @UniqueConstraint(name = "uk_country_code", columnNames = "COUNTRY_CODE") }) public class CntCountry { @Id @SequenceGenerator(name = "CntCountry", sequenceName = "CNT_COUNTRY_ID_SEQ") @GeneratedValue(generator = "CntCountry") private Long id; @Column(name = "COUNTRY_NAME", length = 30, nullable = false) private String countryName; @Column(name = "COUNTRY_CODE", length = 30, nullable = false) private String countryCode; }
注意:如果当前库中已经存在重复的国家记录,先清理重复数据再执行约束添加,否则约束创建会失败。
2. 修正错误的级联配置
城市和国家是多对一关系,删除、更新城市时不应该对国家记录做级联写操作,移除CtyCity实体中@ManyToOne上不合理的级联配置:
@Getter @Setter @Table(name = "CITY") @Entity public class CtyCity { @Id @SequenceGenerator(name = "CtyCity", sequenceName = "CTY_CITY_ID_SEQ") @GeneratedValue(generator = "CtyCity") private Long id; @Column(name = "CITY_NAME", length = 30, nullable = false) private String cityName; @Column(name = "PLATE_NUMBER", length = 30, nullable = false) private Long plateNumber; // 移除cascade = CascadeType.ALL,加上国家非空约束避免脏数据 @ManyToOne(fetch = FetchType.EAGER) @JoinColumn(name = "FK_COUNTRY", nullable = false) private CntCountry country; }
3. 补全DAO层查询方法
在国家对应的DAO接口中,新增根据国家编码查询的方法:
public interface CntCountryDao extends JpaRepository<CntCountry, Long> { // 根据唯一的国家编码查询已有记录 Optional<CntCountry> findByCountryCode(String countryCode); }
4. 改造Service层保存逻辑
保存城市前先做国家存在性判断,已有国家直接关联,不存在再新建:
@Service @AllArgsConstructor public class CtyCityService { private CtyCityDao ctyCityDao; private CntCountryDao cntCountryDao; public CtyCityDto save(CtyCitySaveRequestDto ctyCitySaveRequestDto){ CtyCity ctyCity = CtyCityMapper.INSTANCE.convertToCtyCity(ctyCitySaveRequestDto); // 关联已有国家,避免重复插入 CntCountry requestCountry = ctyCity.getCountry(); CntCountry targetCountry = cntCountryDao.findByCountryCode(requestCountry.getCountryCode()) .orElseGet(() -> cntCountryDao.save(requestCountry)); ctyCity.setCountry(targetCountry); ctyCity = ctyCityDao.save(ctyCity); return CtyCityMapper.INSTANCE.convertToCtyCityDto(ctyCity); } }
问题复现参考截图
- Swagger接口调试页:

- 城市表重复关联数据:

- 国家表重复插入记录:

额外优化建议
- 国家属于基础数据,后续可以单独做国家的增删改查维护接口,新增城市时只需要传国家ID关联即可,不需要每次传国家名称、编码全量信息
- 可以给城市表也加联合唯一约束,比如同一个国家下城市名+车牌编号唯一,避免重复城市数据插入
- MapStruct映射时注意字段匹配规则,避免把已查询到的国家实体ID覆盖成空值导致逻辑失效
内容的提问来源于stack exchange,提问作者Umut umut
相关产品推荐
相关产品推荐

