Spring Boot 3中如何处理实体唯一性校验及异常传递?
结合你的场景需求(保留@Transactional、更新/保存失败时给UI传递提示、解决并发问题),正确的处理方式需要数据库约束为核心,配合业务层预检查+异常捕获,再通过全局异常处理器统一响应UI,具体步骤如下:
1. 先确保数据库和实体类的唯一约束(必做)
这是保证唯一性的根本,无论业务层怎么处理,只有数据库能在并发场景下严格保证唯一性。
实体类配置
@Entity public class Car { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; // 标记名称唯一、非空 @Column(unique = true, nullable = false) private String name; // 其他字段、getter、setter }
数据库层面补充唯一索引(推荐)
虽然Hibernate的@Column(unique=true)会生成DDL语句创建唯一约束,但手动在数据库创建索引更可靠,避免Hibernate自动生成的DDL被忽略:
CREATE UNIQUE INDEX idx_car_name ON car(name); -- 如需忽略大小写,可根据数据库调整,比如MySQL: -- CREATE UNIQUE INDEX idx_car_name ON car(LOWER(name));
2. 业务层优化:预检查+异常捕获(保留@Transactional)
业务层做两件事:
- 先做预检查,减少触发数据库异常的场景,提升用户体验;
- 用
saveAndFlush()立即触发SQL执行,捕获数据库约束异常,抛出自定义业务异常(保证事务回滚)。
业务层代码
@Service public class CarService { private final CarRepository carRepository; private final CarMapper carMapper; public CarService(CarRepository carRepository, CarMapper carMapper) { this.carRepository = carRepository; this.carMapper = carMapper; } @Transactional public CarCreateDto updateCar(CarCreateDto updateCarDto, Long id) { // 1. 查询待更新的车辆 Car car = carRepository.findById(id) .orElseThrow(() -> new EntityNotFoundException("ID为" + id + "的车辆不存在")); // 2. 预检查:如果名称未变更,直接更新;否则检查名称是否已存在 String newName = updateCarDto.getName(); if (!newName.equalsIgnoreCase(car.getName())) { // 预检查名称是否存在,这里用忽略大小写的查询(根据需求调整) if (carRepository.existsByNameIgnoreCase(newName)) { throw new CarAlreadyExistsException("名称为[" + newName + "]的车辆已存在"); } } // 3. 执行更新操作 carMapper.partialUpdate(updateCarDto, car); try { // 用saveAndFlush()立即执行SQL,触发数据库约束检查 carRepository.saveAndFlush(car); } catch (DataIntegrityViolationException e) { // 处理并发场景下的漏网之鱼(比如两个请求同时通过预检查) throw new CarAlreadyExistsException("名称为[" + newName + "]的车辆已存在", e); } return carMapper.toDto(car); } // 保存车辆的逻辑同理 @Transactional public CarCreateDto saveCar(CarCreateDto carCreateDto) { String name = carCreateDto.getName(); // 预检查 if (carRepository.existsByNameIgnoreCase(name)) { throw new CarAlreadyExistsException("名称为[" + name + "]的车辆已存在"); } Car car = carMapper.toEntity(carCreateDto); try { carRepository.saveAndFlush(car); } catch (DataIntegrityViolationException e) { throw new CarAlreadyExistsException("名称为[" + name + "]的车辆已存在", e); } return carMapper.toDto(car); } }
自定义业务异常(必须为RuntimeException)
Spring的@Transactional默认只有RuntimeException才会触发事务回滚,所以自定义异常需要继承RuntimeException:
public class CarAlreadyExistsException extends RuntimeException { public CarAlreadyExistsException(String message) { super(message); } public CarAlreadyExistsException(String message, Throwable cause) { super(message, cause); } }
3. 全局异常处理器:统一响应UI(符合职责划分)
用@RestControllerAdvice实现全局异常处理,控制器无需关心异常,只负责处理请求响应,符合单一职责原则。
全局异常处理器代码
@RestControllerAdvice public class GlobalExceptionHandler { // 处理车辆名称重复的异常 @ExceptionHandler(CarAlreadyExistsException.class) public ResponseEntity<ErrorResponse> handleCarAlreadyExists(CarAlreadyExistsException e) { ErrorResponse error = new ErrorResponse(HttpStatus.CONFLICT.value(), e.getMessage()); return new ResponseEntity<>(error, HttpStatus.CONFLICT); } // 处理实体不存在的异常 @ExceptionHandler(EntityNotFoundException.class) public ResponseEntity<ErrorResponse> handleEntityNotFound(EntityNotFoundException e) { ErrorResponse error = new ErrorResponse(HttpStatus.NOT_FOUND.value(), e.getMessage()); return new ResponseEntity<>(error, HttpStatus.NOT_FOUND); } // 处理参数校验异常 @ExceptionHandler(MethodArgumentNotValidException.class) public ResponseEntity<ErrorResponse> handleValidationErrors(MethodArgumentNotValidException e) { String message = e.getBindingResult().getFieldErrors().stream() .map(error -> error.getField() + ": " + error.getDefaultMessage()) .collect(Collectors.joining("; ")); ErrorResponse error = new ErrorResponse(HttpStatus.BAD_REQUEST.value(), message); return new ResponseEntity<>(error, HttpStatus.BAD_REQUEST); } // 错误响应DTO public static class ErrorResponse { private int code; private String message; public ErrorResponse(int code, String message) { this.code = code; this.message = message; } // getter、setter public int getCode() { return code; } public void setCode(int code) { this.code = code; } public String getMessage() { return message; } public void setMessage(String message) { this.message = message; } } }
对你之前尝试的方案的问题分析
方案1:直接捕获DataIntegrityViolationException
问题在于@Transactional下,save()是延迟执行的(Hibernate一级缓存机制),SQL会在事务提交时才执行,所以业务方法里的try-catch根本捕获不到异常,异常是在事务管理器提交阶段抛出的,不在方法执行范围内。方案2:用saveAndFlush捕获异常
这个方案其实是可行的,所谓“saveAndFlush不宜使用”是指不要在不需要立即持久化的场景下滥用,但在需要立即触发数据库约束检查的场景下,它是正确的选择,不用被这个说法误导。方案3:预检查数据库
预检查只能作为优化(减少触发数据库异常的次数),但不能依赖,因为并发场景下两个请求可能同时通过预检查,然后同时执行更新/插入,最终还是会触发数据库约束异常,所以必须配合数据库约束和异常处理。方案4:控制器中捕获异常
违背了单一职责原则,控制器应该只负责接收请求和返回响应,业务逻辑和异常处理应该交给业务层和全局处理器,这样代码更易维护。
内容的提问来源于stack exchange,提问作者Al Grant

