Nesse post continuaremos falando da JSR-303 Bean Validation, falare-mos sobre validação Cross-Field, haverão situações onde será necessário validar um campo, dependendo do valor de outro, também podemos tirar vantagem da JSR-303 nesse cenário, usaremos os validadores de classe
# Validação baseada em múltiplos campos
Veja o seguinte validator/anotação, usado para verificar a unicidade do campo
userName
na classe User
@Entity
@Table(name = "USER")
public class User implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
@NotBlank
private String fullName;
@NotBlank
private String preferredName;
@UniqueUsername
@NotBlank
private String userName;
// Getters/Setters
}
@Documented
@Constraint(validatedBy = UniqueUsernameValidator.class)
@Target({ ElementType.PARAMETER, ElementType.FIELD, ElementType.ANNOTATION_TYPE })
@Retention(RetentionPolicy.RUNTIME)
public @interface UniqueUsername {
String message() default "already in use";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
@Component
public class UniqueUsernameValidator implements ConstraintValidator<UniqueUsername,String> {
private UserRepository userRepository;
@Autowired
public UniqueUsernameValidator(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
if(!isEmpty(value)){
return !userRepository.existsByUserName(value);
}
return true;
}
}
Consegue ver o problema?! Se usarmos a anotação no atributo userName
o validador recebera apenas o valor do campo, e no caso de uma operação
de atualização, podemos ter uma falsa falha na validação,
no caso de informar o mesmo userName
para o registro que desejamos
atualizar. Veja:
{
"id": 1,
//other fields
"userName": "sameExistingUsername"
}
Se enviarmos esse payload para o endpoint de atualização, o campo userName
lançará um erro
de ConstraintViolation, pois, o método userRepository.existsByUserName(value)
vai retornar true.
O problema aqui é que não levamos em consideração o id
quando ele existe, para isso,
vamos alterar nosso validador
@Component
public class UniqueUsernameValidator implements ConstraintValidator<UniqueUsername, User> {
private UserRepository userRepository;
@Autowired
public UniqueUsernameValidator(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Override
public boolean isValid(User value, ConstraintValidatorContext context) {
if(!isEmpty(value)){
boolean exists = false;
if(value.getId() != null) {
exists = userRepository.existsByUserNameEqualsAndIdNot(value.getUserName() , value.getId());
}else {
exists = userRepository.existsByUserName(value.getUserName());
}
if(exists){
context.disableDefaultConstraintViolation();
context.buildConstraintViolationWithTemplate(UniqueUsername.MESSAGE)
.addPropertyNode("userName")
.addConstraintViolation();
return false;
}
}
return true;
}
}
Agora o validador espera a classe User
, ao invés de String
,
com isso teremos acesso a todos os atributos em User
, e conseguimos
fazer nossa validação como bem entendermos.
Como agora estamos validando a classe, e queremos marcar um campo específico como errado, usamos
context.disableDefaultConstraintViolation()
para desabilitar o comportamento padrão,
e usamos:
// iniciamos o builder com a mensagem padrão
context.buildConstraintViolationWithTemplate(UniqueUsername.MESSAGE)
.addPropertyNode("userName")// Marcamos o atributo violado
.addConstraintViolation(); // adicionamos a violação manualmente
Para que tudo funcione como esperado, também temos que atualizar nossa anotação:
@Documented
@Constraint(validatedBy = UniqueUsernameValidator.class)
@Target({ ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
public @interface UniqueUsername {
String MESSAGE = "already in use";
String message() default MESSAGE;
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
Com isso, sinalizamos que essa anotação só pode ser usada a nível de classe. Por fim temos nosso modelo
@UniqueUsername
@Entity
@Table(name = "USER")
public class User implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
@NotBlank
private String fullName;
@NotBlank
private String preferredName;
@NotBlank
private String userName;
// Getters/Setters
}
O projeto completo no seguinte link