Como posso validar dois ou mais campos em combinação?

92

Estou usando a validação JPA 2.0 / Hibernate para validar meus modelos. Agora tenho uma situação em que a combinação de dois campos deve ser validada:

public class MyModel {
    public Integer getValue1() {
        //...
    }
    public String getValue2() {
        //...
    }
}

O modelo é inválido se ambos getValue1()e getValue2()são nulle válido caso contrário.

Como posso realizar este tipo de validação com JPA 2.0 / Hibernate? Com uma @NotNullanotação simples, ambos os getters devem ser não nulos para passar na validação.

Daniel Rikowski
fonte

Respostas:

102

Para validação de várias propriedades, você deve usar restrições de nível de classe. Do Bean Validation Sneak Peek parte II: restrições personalizadas :

### Restrições de nível de classe

Alguns de vocês expressaram preocupações sobre a capacidade de aplicar uma restrição abrangendo várias propriedades ou de expressar uma restrição que depende de várias propriedades. O exemplo clássico é a validação de endereço. Os endereços têm regras intrincadas:

  • o nome de uma rua é um pouco padrão e certamente deve ter um limite de comprimento
  • a estrutura do código postal depende inteiramente do país
  • a cidade pode muitas vezes ser correlacionada a um código postal e alguma verificação de erro pode ser feita (desde que um serviço de validação esteja acessível)
  • por causa dessas interdependências, uma restrição simples de nível de propriedade se ajusta à conta

A solução oferecida pela especificação Bean Validation é dupla:

  • ele oferece a capacidade de forçar a aplicação de um conjunto de restrições antes de outro conjunto de restrições por meio do uso de grupos e sequências de grupos. Este assunto será abordado na próxima entrada do blog
  • permite definir restrições de nível de classe

As restrições de nível de classe são restrições regulares (anotação / implementação duo) que se aplicam a uma classe ao invés de uma propriedade. Dito de outra forma, as restrições de nível de classe recebem a instância do objeto (em vez do valor da propriedade) em isValid.

@AddressAnnotation 
public class Address {
    @NotNull @Max(50) private String street1;
    @Max(50) private String street2;
    @Max(10) @NotNull private String zipCode;
    @Max(20) @NotNull String city;
    @NotNull private Country country;
    
    ...
}

@Constraint(validatedBy = MultiCountryAddressValidator.class)
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface AddressAnnotation {
    String message() default "{error.address}";
    Class<?>[] groups() default { };
    Class<? extends Payload>[] payload() default { };
}

public class MultiCountryAddressValidator implements ConstraintValidator<AddressAnnotation, Address> {
    public void initialize(AddressAnnotation constraintAnnotation) {
    // initialize the zipcode/city/country correlation service
    }

    /**
     * Validate zipcode and city depending on the country
     */
    public boolean isValid(Address object, ConstraintValidatorContext context) {
        if (!(object instanceof Address)) {
            throw new IllegalArgumentException("@Address only applies to Address");
        }
        Address address = (Address) object;
        Country country = address.getCountry();
        if (country.getISO2() == "FR") {
            // check address.getZipCode() structure for France (5 numbers)
            // check zipcode and city correlation (calling an external service?)
            return isValid;
        } else if (country.getISO2() == "GR") {
            // check address.getZipCode() structure for Greece
            // no zipcode / city correlation available at the moment
            return isValid;
        }
        // ...
    }
}

As regras de validação de endereço avançadas foram deixadas de fora do objeto de endereço e implementadas por MultiCountryAddressValidator. Ao acessar a instância do objeto, as restrições de nível de classe têm muita flexibilidade e podem validar várias propriedades correlacionadas. Observe que a ordenação foi deixada de fora da equação aqui, vamos voltar a ela no próximo post.

O grupo de especialistas discutiu várias abordagens de suporte a propriedades múltiplas: achamos que a abordagem de restrição de nível de classe fornece simplicidade e flexibilidade suficientes em comparação com outras abordagens de nível de propriedade envolvendo dependências. Seu feedback é bem-vindo.

Pascal Thivent
fonte
17
A interface ConstraintValidator e a anotação @Constraint foram invertidas no exemplo. E é válido () leva 2 parâmetros.
Guillaume Husta
1
TYPEe RUNTIMEdeve ser substituído por ElementType.TYPEe RetentionPolicy.RUNTIME, respectivamente.
mark.monteiro
2
@ mark.monteiro Você pode usar importações estáticas: import static java.lang.annotation.ElementType.*;eimport static java.lang.annotation.RetentionPolicy.*;
cassiomolin
2
Reescrevi o exemplo para funcionar com a Validação do Bean. Dê uma olhada aqui .
cassiomolina
1
Os parâmetros de anotação não estão dentro da especificação correta, pois deve haver uma mensagem, grupos e carga útil como foi mencionado por Cássio nesta resposta.
Peter S.
38

Para funcionar corretamente com a validação de feijão , o exemplo fornecido na resposta de Pascal Thivent pode ser reescrito da seguinte forma:

@ValidAddress
public class Address {

    @NotNull
    @Size(max = 50)
    private String street1;

    @Size(max = 50)
    private String street2;

    @NotNull
    @Size(max = 10)
    private String zipCode;

    @NotNull
    @Size(max = 20)
    private String city;

    @Valid
    @NotNull
    private Country country;

    // Getters and setters
}
public class Country {

    @NotNull
    @Size(min = 2, max = 2)
    private String iso2;

    // Getters and setters
}
@Documented
@Target(TYPE)
@Retention(RUNTIME)
@Constraint(validatedBy = { MultiCountryAddressValidator.class })
public @interface ValidAddress {

    String message() default "{com.example.validation.ValidAddress.message}";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};
}
public class MultiCountryAddressValidator 
       implements ConstraintValidator<ValidAddress, Address> {

    public void initialize(ValidAddress constraintAnnotation) {

    }

    @Override
    public boolean isValid(Address address, 
                           ConstraintValidatorContext constraintValidatorContext) {

        Country country = address.getCountry();
        if (country == null || country.getIso2() == null || address.getZipCode() == null) {
            return true;
        }

        switch (country.getIso2()) {
            case "FR":
                return // Check if address.getZipCode() is valid for France
            case "GR":
                return // Check if address.getZipCode() is valid for Greece
            default:
                return true;
        }
    }
}
cassiomolina
fonte
Como inicializar ou chamar o validador customizado em um projeto Restful do WebSphere para um bean CDI? Eu escrevi tudo, mas a restrição personalizada não está funcionando ou invoquei
BalaajiChander
Estou preso a uma validação semelhante, mas a minha isoA2Codeestá armazenada na Countrytabela do banco de dados . É uma boa ideia fazer uma chamada de DB daqui? Além disso, gostaria de vinculá-los após a validação porque Address belongs_toum Countrye quero que a addressentrada tenha countrya chave estrangeira da tabela. Como eu vincularia o país ao endereço?
krozaine
Observe que quando você definir uma anotação de validação de tipo em um objeto errado, uma exceção será lançada pela estrutura de validação do Bean. Por exemplo, se você definir a @ValidAddressanotação no objeto País, você obterá uma No validator could be found for constraint 'com.example.validation.ValidAddress' validating type 'com.example.Country'exceção.
Jacob van Lingen
12

Um validador de nível de classe customizado é o caminho a percorrer, quando você deseja ficar com a especificação de validação de feijão, exemplo aqui .

Se você quiser usar um recurso do Hibernate Validator, pode usar @ScriptAssert , que é fornecido desde o Validator-4.1.0.Final. Exceto de seu JavaDoc:

Expressões de script podem ser escritas em qualquer linguagem de script ou expressão, para a qual um mecanismo compatível com JSR 223 ("Scripting para a plataforma JavaTM") pode ser encontrado no classpath.

Exemplo:

@ScriptAssert(lang = "javascript", script = "_this.value1 != null || _this != value2)")
public class MyBean {
  private String value1;
  private String value2;
}
Hardy
fonte
Sim, e o Java 6 inclui Rhino (mecanismo JavaScript) para que você possa usar JavaScript como a linguagem de expressão sem adicionar dependências extras.
3
Aqui está um exemplo de como criar tal validação com Hibernate Validator 5.1.1.Final
Ivan Hristov