How to catch check for Uniqueness in Spring Boot 3
Asked Answered
U

0

2

In my Spring Boot 3 project with hibernate 6 I am trying to check the a name is not in use before updating (and saving) a entity.

I want to leave the method annotated with @Transactional and I want to be able to pass a message to the UI if the update/save fails.

There is a number of existing answers, but they all have issues. Here is what I have tried:

1. Catch DataIntegtrityException

This way throws a custom exception - the con is that it doesn't work with @Transactional

    //@Transactional - doesn't catch exception with @Transactional
    public CarCreateDto updateCar(CarCreateDto updateCarDto, Long id) {
        Car Car = CarRepository.findById(id).orElseThrow(() -> new EntityNotFoundException("Car with " + id + " not found"));
        try {
            CarMapper.partialUpdate(updateCarDto, Car);
            CarRepository.save(Car);
            return CarMapper.toDto(Car);
        } catch (DataIntegrityViolationException e) {
            throw new CarAlreadyExistsException("Car with name already exist");
        }
    }

2. Catch DataIntegtrityException with saveAndFlush

Same as above but uses saveAndFlush and works with @Transactional. Can't find the reference but the con is that saveAndFlush is not good to use.

    @Transactional
    public CarCreateDto updateCar(CarCreateDto updateCarDto, Long id) {
        Car Car = CarRepository.findById(id).orElseThrow(() -> new EntityNotFoundException("Car with " + id + " not found"));
        try {
            CarMapper.partialUpdate(updateCarDto, Car);
            CarRepository.save(Car);
            return CarMapper.toDto(Car);
        } catch (DataIntegrityViolationException e) {
            throw new CarAlreadyExistsException("Car with name already exist");
        }
    }

3. Run a pre-save check on the database

The con here is a extra call to the database. Also potential for concurrency issues.

    @Transactional
    public CarCreateDto updateCar(CarCreateDto updateCarDto, Long id) {
    Car Car = CarRepository.findById(id).orElseThrow(() -> new EntityNotFoundException("Car with " + id + " not found"));
    boolean samename = updateCarDto.getName().equalsIgnoreCase(Car.getName());
    if (samename || !samename && !CarRepository.existsByName(updateCarDto.getName())) {
        CarMapper.partialUpdate(updateCarDto, Car);
        CarRepository.save(Car);
        return CarMapper.toDto(Car);
    } else {
        throw new DatabaseUniqueConstraintException("Car with name " + updateCarDto.getName() + " already exists");
    }
}

4. Put the catch on the controller

This just feels wrong - not a controller responsibility to catch DB exceptions.

@PutMapping(value = "/{id}", consumes = "application/json")
ResponseEntity<CarCreateDto> updateCarDto(@Valid @RequestBody CarCreateDto updateCar, @PathVariable Long id) {
    System.out.println("wait");
    try {
        CarCreateDto Car = CarService.updateCar(updateCar, id);
        return new ResponseEntity<>(Car, HttpStatus.OK);
    } catch (DataIntegrityViolationException e) {
        throw new CarAlreadyExistsException("Car with name already exist");
    }
}

So what is the correct way to handle this exception for a uniqueness check?

Usage answered 9/10, 2023 at 7:37 Comment(4)
have you defined unique key in DB? then by using one DB call at the time of save/update if name is different then it'll save/update else it'll throw DataIntegrityViolationExceptionTranscribe
That is the approach in solution #3 where !samename && !CarRepository.existsByName(updateCarDto.getName()) uses a @Query existsByName to check and return boolean.Usage
why your checking with existsByName, just save directly and let DB use the unique key and throw ORA-00001: unique constraint which you can catch, if you want to have less DB callTranscribe
Because catching exceptions with @Transactional present doesnt work for some reason. See 4th comment on this post. Cant catch until whole method has runUsage

© 2022 - 2024 — McMap. All rights reserved.