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

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接口调试页:
    swaggerui
  • 城市表重复关联数据:
    psql city
  • 国家表重复插入记录:
    psql country

额外优化建议

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

内容的提问来源于stack exchange,提问作者Umut umut

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.30 16:03:34