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

Spring Boot中处理PostgreSQL唯一约束重复键异常

问题:PostgreSQL唯一约束冲突异常的定位与处理

一、环境与场景说明

1. 数据库表结构

PostgreSQL中user_asset表包含id、user_id、code字段,code字段存在user_asset_code_unique唯一约束,现有数据如下:

user_asset

id  user_id  code
1     97     7752
2     98     7753 
3     99     7754

2. Spring Boot相关代码

Repository层:

@Repository
public interface AssetRepository extends JpaRepository<UserAsset, Integer> {}

Entity实体类:

@Entity
@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Table(name = "user_asset", schema = "public")
public class UserAsset {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id")
    private Integer id;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "user_id")
    private User user;

    @OneToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "code")
    private MPID mpid;
}

业务逻辑代码:

private final AssetRepository assetRepository;

@Transactional
public UserAssetResponse update(UserAssetRequest dto){
    // 其他业务代码

    User user = userRepository.findById(dto.getID());

    addAssets(user, dto.getAssets());
} 

private void addAssets(User user, List<Long> assets){

    List<UserAsset> newAssets = mpidRepository.findAllById(assets).stream()
            .map(item -> UserAsset.builder().user(user).mpid(item).build())
            .collect(Collectors.toList());
    
    assetRepository.saveAll(newAssets);
}

3. 请求与预期操作

通过Postman发送更新请求,期望将用户97的资产替换为[7753,7754]:

{
    "id": 97,
    "assets": [7753,7754]
}

当前用户97的资产记录仅为:

id  user_id  code
1     97     7752

二、异常信息

Postman返回500错误:

500 Internal server error
{
    "message": "could not execute statement; SQL [n/a]; constraint [user_asset_code_unique]; nested exception is org.hibernate.exception.ConstraintViolationException: could not execute statement"
}

IDEA错误日志:

o.h.engine.jdbc.spi.SqlExceptionHelper   : ERROR: duplicate key value violates unique constraint "user_asset_code_unique"
  Detail: Key (mpid_id)=(7753) already exists.

用户尝试通过try-catch捕获异常但无效:

try{
  assetRepository.saveAll(newAssets);          
}catch (Exception e){
  e.printStackTrace();
}

三、疑问

已知异常由code字段的唯一约束导致,但不清楚异常发生位置,且try-catch无法捕获,需要明确异常定位方法及正确处理方案。

四、异常定位方法

  1. 从日志直接定位根源:日志明确指出Key (mpid_id)=(7753) already exists,说明7753这个code已被用户98占用(原表中user_id=98的code正是7753),由于code字段的唯一约束,无法重复插入该值。
  2. 解释try-catch无效原因:因为业务方法带有@Transactional注解,saveAll执行后不会立即提交SQL,事务会在方法结束时统一提交,此时抛出的异常不在你写的try-catch范围内,被Spring事务管理器拦截,所以你的try块无法捕获到异常。

五、正确处理方案

1. 全量更新:替换用户所有资产

如果需求是将用户资产替换为新列表,先删除用户原有资产,再插入新资产:

private void addAssets(User user, List<Long> assets){
    // 删除用户原有资产
    assetRepository.deleteByUser(user);
    // 构建并保存新资产
    List<UserAsset> newAssets = mpidRepository.findAllById(assets).stream()
            .map(item -> UserAsset.builder().user(user).mpid(item).build())
            .collect(Collectors.toList());
    assetRepository.saveAll(newAssets);
}

需在AssetRepository中新增删除方法:

@Modifying
@Query("DELETE FROM UserAsset ua WHERE ua.user = :user")
void deleteByUser(@Param("user") User user);

2. 增量更新:仅添加未存在的资产

如果需求是在原有资产基础上新增,先过滤掉用户已有的资产ID,再插入:

private void addAssets(User user, List<Long> assets){
    // 查询用户已拥有的资产code列表
    List<Long> existingCodes = assetRepository.findByUser(user).stream()
            .map(ua -> ua.getMpid().getId())
            .collect(Collectors.toList());
    // 过滤掉已存在的资产ID
    List<Long> newAssetIds = assets.stream()
            .filter(id -> !existingCodes.contains(id))
            .collect(Collectors.toList());
    // 构建并保存新资产
    List<UserAsset> newAssets = mpidRepository.findAllById(newAssetIds).stream()
            .map(item -> UserAsset.builder().user(user).mpid(item).build())
            .collect(Collectors.toList());
    assetRepository.saveAll(newAssets);
}

需在AssetRepository中新增查询方法:

List<UserAsset> findByUser(User user);

3. 正确捕获事务内的异常

若需要捕获约束冲突异常,需手动管理事务,比如使用TransactionTemplate:

private final TransactionTemplate transactionTemplate;

private void addAssets(User user, List<Long> assets){
    transactionTemplate.execute(status -> {
        try {
            List<UserAsset> newAssets = mpidRepository.findAllById(assets).stream()
                    .map(item -> UserAsset.builder().user(user).mpid(item).build())
                    .collect(Collectors.toList());
            assetRepository.saveAll(newAssets);
            return null;
        } catch (ConstraintViolationException e) {
            // 处理异常,比如记录日志、返回提示
            e.printStackTrace();
            status.setRollbackOnly(); // 标记事务回滚
            return null;
        }
    });
}

或者将@Transactional注解移至包含try-catch的方法,并确保异常不会被Spring吞掉(例如在catch块中重新抛出运行时异常)。

内容的提问来源于stack exchange,提问作者Roman Chumak

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.21 20:26:00