How to handle DataIntegrityViolationException in Spring?
Asked Answered
H

4

26

I need to show custom messages in my Spring 3.0 application. I have a database with Hibernate and there are several constraints. I have doubts in how DataIntegrityViolationException should be handled in a good way. I wonder if there is a way to map the exception with a message set in a properties file, as it is possible in Constraints validation. Could I handle it automatically in any way or I have to catch this exception in each controller?

Holm answered 21/1, 2010 at 13:25 Comment(0)
B
21

The problem with showing user-friendly messages in the case of constraint violation is that the constraint name is lost when Hibernate's ConstraintViolationException is being translated into Spring's DataIntegrityViolationException.

However, you can customize this translation logic. If you use LocalSessionFactoryBean to access Hibernate, you can supply it with a custom SQLExceptionTranslator (see LocalSessionFactoryBean.jdbcExceptionTranslator). This exception translator can translate a ConstraintViolationException into your own exception class, preserving the constraint name.

Boresome answered 21/1, 2010 at 16:21 Comment(1)
Do you have a example of this?Instil
D
18

I treat DataIntegrityViolationException in ExceptionInfoHandler, finding DB constraints occurrences in root cause message and convert it into i18n message via constraintCodeMap:

@RestControllerAdvice
public class ExceptionInfoHandler {

  @Autowired
  private final MessageSourceAccessor messageSourceAccessor;

  private static Map<String, String> CONSTRAINS_I18N_MAP = Map.of(
      "users_unique_email_idx", EXCEPTION_DUPLICATE_EMAIL,
      "meals_unique_user_datetime_idx", EXCEPTION_DUPLICATE_DATETIME);

  @ResponseStatus(value = HttpStatus.CONFLICT)  // 409
  @ExceptionHandler(DataIntegrityViolationException.class)
  public ErrorInfo conflict(HttpServletRequest req, DataIntegrityViolationException e) {
    String rootMsg = ValidationUtil.getRootCause(e).getMessage();
    if (rootMsg != null) {
        String lowerCaseMsg = rootMsg.toLowerCase();
        for (Map.Entry<String, String> entry : CONSTRAINS_I18N_MAP.entrySet()) {
            if (lowerCaseMsg.contains(entry.getKey())) {
                return logAndGetErrorInfo(req, e, VALIDATION_ERROR, messageSourceAccessor.getMessage(entry.getValue()));
            }
        }
    }
    return logAndGetErrorInfo(req, e, DATA_ERROR);
  }

  //  https://mcmap.net/q/225569/-java-find-the-first-cause-of-an-exception
  @NonNull
  private static Throwable getRootCause(@NonNull Throwable t) {
    Throwable rootCause = NestedExceptionUtils.getRootCause(t);
    return rootCause != null ? rootCause : t;
  }
  ...
}

Can be simulated in my Java Enterprise training application by adding/editing user with duplicate mail or meal with duplicate dateTime.

UPDATE:

Other solution: use Controller Based Exception Handling:

@RestController
@RequestMapping("/ajax/admin/users")
public class AdminAjaxController {

    @ExceptionHandler(DataIntegrityViolationException.class)
    public ResponseEntity<ErrorInfo> duplicateEmailException(HttpServletRequest req, DataIntegrityViolationException e) {
        return exceptionInfoHandler.getErrorInfoResponseEntity(req, e, EXCEPTION_DUPLICATE_EMAIL, HttpStatus.CONFLICT);
    }
Deathly answered 23/2, 2017 at 17:33 Comment(2)
Did you have a different config to get that constraint friendly? My constraint are something like this UK_6DOTKOTT2KJSP8VW4D0M25FB7_INDEX_4Jag
There are several way to create readable constraints: 1. In initial SQL script. 2. DB auto generation with uniqueConstraints parameter in @Table annotation, e.g. @Table(name = "meals", uniqueConstraints = {@UniqueConstraint(columnNames = {"user_id", "date_time"}, name = "meals_unique_user_datetime_idx")})Deathly
C
4

Spring 3 provides two ways of handling this - HandlerExceptionResolver in your beans.xml, or @ExceptionHandler in your controller. They both do the same thing - they turn the exception into a view to render.

Both are documented here.

Crissy answered 21/1, 2010 at 14:19 Comment(0)
T
0

1. In your request body class check for not null or not empty like this

public class CustomerRegisterRequestDto {
    @NotEmpty(message = "first name is empty")
    @NotNull(message = Constants.EntityValidators.FIRST_NAME_NULL)
    private String first_name;
    //other fields
    //getters and setters
}

2. Then in your service check for this

ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
    Validator validator = factory.getValidator();
    Set<ConstraintViolation<CustomerRegisterRequestDto>> violations = validator.validate(userDto);
    if (!violations.isEmpty()) {
        //something is wrong in request parameters
        List<String> details = new ArrayList<>();
        for (ConstraintViolation<CustomerRegisterRequestDto> violation : violations) {
            details.add(violation.getMessage());
        }
        ErrorResponse error = new ErrorResponse(Constants.ErrorResponse.REQUEST_PARAM_ERROR, details);
        return new ResponseEntity<>(error, HttpStatus.BAD_REQUEST);
    }

3. Here is your ErrorResponse class

Thrombocyte answered 7/11, 2019 at 12:6 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.