¿Cómo puedo validar dos o más campos en combinación?

92

Estoy usando la validación JPA 2.0 / Hibernate para validar mis modelos. Ahora tengo una situación en la que se debe validar la combinación de dos campos:

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

El modelo no es válido si ambos getValue1()y getValue2()son nullválidos en caso contrario.

¿Cómo puedo realizar este tipo de validación con JPA 2.0 / Hibernate? Con una @NotNullanotación simple, ambos captadores deben ser no nulos para pasar la validación.

Daniel Rikowski
fuente
2
posible duplicado de la validación de campo cruzado con Hibernate Validator (JSR 303)
Steve Chambers

Respuestas:

102

Para la validación de múltiples propiedades, debe usar restricciones de nivel de clase. De Bean Validation Sneak Peek parte II: restricciones personalizadas :

### Restricciones a nivel de clase

Algunos de ustedes han expresado inquietudes sobre la capacidad de aplicar una restricción que abarque varias propiedades, o de expresar una restricción que dependa de varias propiedades. El ejemplo clásico es la validación de direcciones. Las direcciones tienen reglas complejas:

  • el nombre de una calle es algo estándar y ciertamente debe tener un límite de longitud
  • la estructura del código postal depende completamente del país
  • la ciudad a menudo se puede correlacionar con un código postal y se puede realizar alguna verificación de errores (siempre que se pueda acceder a un servicio de validación)
  • Debido a estas interdependencias, una simple restricción de nivel de propiedad se ajusta a la factura.

La solución ofrecida por la especificación Bean Validation es doble:

  • Ofrece la capacidad de forzar la aplicación de un conjunto de restricciones antes que otro conjunto de restricciones mediante el uso de grupos y secuencias de grupos. Este tema se tratará en la próxima entrada del blog.
  • permite definir restricciones a nivel de clase

Las restricciones de nivel de clase son restricciones regulares (dúo de anotación / implementación) que se aplican a una clase en lugar de a una propiedad. Dicho de otra manera, las restricciones de nivel de clase reciben la instancia del objeto (en lugar del valor de la propiedad) en 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;
        }
        // ...
    }
}

Las reglas avanzadas de validación de direcciones se dejaron fuera del objeto de dirección y fueron implementadas por MultiCountryAddressValidator. Al acceder a la instancia del objeto, las restricciones de nivel de clase tienen mucha flexibilidad y pueden validar múltiples propiedades correlacionadas. Tenga en cuenta que el orden se deja fuera de la ecuación aquí, volveremos a él en la próxima publicación.

El grupo de expertos ha discutido varios enfoques de soporte de propiedades múltiples: creemos que el enfoque de restricción de nivel de clase proporciona suficiente simplicidad y flexibilidad en comparación con otros enfoques de nivel de propiedad que involucran dependencias. Sus comentarios son bienvenidos.

Pascal Thivent
fuente
17
La interfaz ConstraintValidator y la anotación @Constraint se han invertido en el ejemplo. Y es válido () toma 2 parámetros.
Guillaume Husta
1
TYPEy RUNTIMEdebe reemplazarse por ElementType.TYPEy RetentionPolicy.RUNTIME, respectivamente.
mark.monteiro
2
@ mark.monteiro Puede utilizar importaciones estáticas: import static java.lang.annotation.ElementType.*;yimport static java.lang.annotation.RetentionPolicy.*;
cassiomolin
2
He reescrito el ejemplo para trabajar con Bean Validation. Echa un vistazo aquí .
cassiomolin
1
Los parámetros de anotación no están dentro de la especificación correcta, porque tiene que haber un mensaje, grupos y carga útil como lo mencionó Cassio en esta respuesta.
Peter S.
38

Para funcionar correctamente con Bean Validation , el ejemplo proporcionado en la respuesta de Pascal Thivent podría reescribirse de la siguiente manera:

@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
fuente
¿Cómo arrancar o invocar un validador personalizado en un proyecto restful de WebSphere para un bean CDI? He escrito todo, pero la restricción personalizada no funciona o no se invoca
BalaajiChander
Estoy atascado con una validación similar, pero mi isoA2Codeestá almacenado en la Countrytabla DB . ¿Es una buena idea hacer una llamada DB desde aquí? Además, me gustaría vincularlos después de la validación porque Address belongs_toa Countryy quiero que la addressentrada tenga countryla clave externa de la tabla. ¿Cómo vincularía el país a la dirección?
krozaine
Tenga en cuenta que cuando establezca una anotación de validación de tipo en un objeto incorrecto, el marco de validación de Bean lanzará una excepción. Por ejemplo, si estableciera la @ValidAddressanotación en el objeto País, obtendría una No validator could be found for constraint 'com.example.validation.ValidAddress' validating type 'com.example.Country'excepción.
Jacob van Lingen
12

Un validador de nivel de clase personalizado es el camino a seguir, cuando desea permanecer con la especificación de validación de Bean, ejemplo aquí .

Si está contento de usar una función de Hibernate Validator, puede usar @ScriptAssert , que se proporciona desde Validator-4.1.0.Final. Excepcional de su JavaDoc:

Las expresiones de script se pueden escribir en cualquier lenguaje de expresión o script, para lo cual se puede encontrar un motor compatible con JSR 223 ("Scripting para la plataforma JavaTM") en la ruta de clases.

Ejemplo:

@ScriptAssert(lang = "javascript", script = "_this.value1 != null || _this != value2)")
public class MyBean {
  private String value1;
  private String value2;
}
Resistente
fuente
Sí, y Java 6 incluye Rhino (motor JavaScript) para que pueda utilizar JavaScript como lenguaje de expresión sin agregar dependencias adicionales.
3
Aquí hay un ejemplo de cómo crear una validación de este tipo con Hibernate Validator 5.1.1.Final
Ivan Hristov