Cleanly map DB constraint violations, no mimicking constraints in app

1 day ago 3
ARTICLE AD BOX

user.name has a UNIQUE constraint on the DB (PG).

I catch DataIntegrityViolationException in my global handler:

@ExceptionHandler(DataIntegrityViolationException.class) public ResponseEntity<ProblemDetail> handle(DataIntegrityViolationException ex) { ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail(HttpStatus.UNPROCESSABLE_CONTENT, ex.getMessage()); return ResponseEntity.unprocessableContent().body(problemDetail); }

An example response on attempting to insert a user with the same username:

{ "detail": "could not execute statement [ОШИБКА: повторяющееся значение ключа нарушает ограничение уникальности \"user_name_key\"\n Подробности: Ключ \"(name)=(jack)\" уже существует.] [insert into \"user\" (name,password,id) values (?,?,?)]; SQL [insert into \"user\" (name,password,id) values (?,?,?)]; constraint [null]", "instance": "/api/admin/users", "status": 422, "title": "Unprocessable Content" }

Note how verbose, clunky the detail is. Besides, it's in Russian (locale, I reckon), but I return errors in English.

What do you suggest? Manually validating it in my service and throwing the exception by hand? But then I would have to manually keep DB constraints and my validation logic in sync. For example, if there's a new UNIQUE column, I would have to update, rebuild the service. Doesn't look optimal.

Claude Sonnet suggested this:

private String resolveDetail(DataIntegrityViolationException ex) { if (ex.getCause() instanceof ConstraintViolationException cve) { return switch (cve.getSQLState()) { case "23505" -> resolveUniqueViolation(cve.getConstraintName()); case "23503" -> "Operation violates a referential integrity constraint"; case "23502" -> "A required field is missing"; case "23514" -> "A value failed a check constraint"; default -> "A database constraint was violated"; }; } return "Data integrity violation"; } private String resolveUniqueViolation(String constraintName) { return appProperties.getConstraintMessages() .getOrDefault(constraintName, "A unique constraint was violated"); } app: constraint-messages: user_name_key: "Username is already taken"

You can imagine I'm not thrilled by this approach.

I can do this:

@ExceptionHandler(ConstraintViolationException.class) public ResponseEntity<ProblemDetail> handle(ConstraintViolationException ex) { String message = String.format("%s constraint violation", ex.getKind()); ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail(HttpStatus.UNPROCESSABLE_CONTENT, message); return ResponseEntity.unprocessableContent().body(problemDetail); } { "detail": "UNIQUE constraint violation", "instance": "/api/admin/users", "status": 422, "title": "Unprocessable Content" }

The drawback is it's not informative and couples my app to Hibernate more (ConstraintViolationException is Hibernate's).

Spring Boot 4, Web.

Read Entire Article