使用JPA/Hibernate插入CityHistory时如何复用已存在的城市记录?
解决方案
你当前遇到重复尝试插入City记录的核心原因是CityHistory实体中cityId字段的@ManyToOne注解配置了cascade = CascadeType.ALL,保存CityHistory时会默认级联持久化关联的City对象。如果关联的City是未被JPA上下文管理的临时对象,就算数据库中已经存在对应identification的记录,JPA也会尝试执行INSERT语句,触发唯一键冲突,可按以下步骤改造:
1. 调整实体类级联配置
首先移除CityHistory中不必要的级联持久化配置,避免无差别级联插入City:
@ManyToOne(fetch = FetchType.LAZY) // 移除 cascade = CascadeType.ALL 配置,不需要级联操作City实体 @JoinColumn(name = "CITY_ID", nullable = false, foreignKey = @ForeignKey(name = "FK_CITY_ID")) private City cityId;
2. 新增City查询接口
给City对应的Repository新增按唯一键identification查询的方法:
public interface CityRepository extends JpaRepository<City, Long> { // 按城市唯一标识查询城市记录 Optional<City> findByIdentification(String identification); // 批量查询用(可选,适配大数据量场景) List<City> findByIdentificationIn(Collection<String> identifications); }
3. 修改ItemWriter写入逻辑
调整InfoItemWriter的写入逻辑,写入前先判断对应城市是否存在,存在则复用,不存在再新增:
@Component public class InfoItemWriter implements ItemWriter<Object> { @Autowired private CityHistoryRepository cityHistoryRepository; @Autowired private CityRepository cityRepository; @Override @Transactional(rollbackFor = Exception.class) public void write(List<? extends Object> items) throws Exception { if (items.isEmpty() || !(items.get(0) instanceof CityHistory)) { return; } List<CityHistory> cityHistories = (List<? extends CityHistory>) items; // 大数据量场景优化:先批量查询所有已存在的城市,减少DB查询次数 Set<String> identifications = cityHistories.stream() .map(h -> h.getCityId().getIdentification()) .collect(Collectors.toSet()); Map<String, City> identToCityMap = cityRepository.findByIdentificationIn(identifications) .stream() .collect(Collectors.toMap(City::getIdentification, Function.identity())); for (CityHistory history : cityHistories) { City city = history.getCityId(); City existCity = identToCityMap.get(city.getIdentification()); if (existCity != null) { // 存在则复用已有记录 history.setCityId(existCity); } else { // 不存在则先保存新城市记录 city = cityRepository.save(city); history.setCityId(city); // 同批次新增的城市存入map,避免同批次重复插入 identToCityMap.put(city.getIdentification(), city); } } // 批量保存所有城市历史记录 cityHistoryRepository.saveAll(cityHistories); } }
4. 并发场景优化(可选)
如果存在并发写入相同城市的场景,可给查询方法加悲观锁避免并发插入冲突:
@Lock(LockModeType.PESSIMISTIC_WRITE) Optional<City> findByIdentification(String identification);
也可以直接捕获唯一键冲突异常,二次查询复用已有记录做兜底。
内容的提问来源于stack exchange,提问作者Ali
相关产品推荐
相关产品推荐

