web-dev-qa-db-fra.com

Comment puis-je valider deux ou plusieurs champs en combinaison?

J'utilise la validation JPA 2.0/Hibernate pour valider mes modèles. J'ai maintenant une situation où la combinaison de deux champs doit être validée:

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

Le modèle est invalide si les deux getValue1() et getValue2() sont null et valides sinon.

Comment puis-je effectuer ce type de validation avec JPA 2.0/Hibernate? Avec une simple annotation @NotNull, Les deux getters doivent être non nuls pour passer la validation.

70
Daniel Rikowski

Pour la validation de plusieurs propriétés, vous devez utiliser des contraintes au niveau de la classe. De Bean Validation Sneak Peek partie II: contraintes personnalisées :

Contraintes au niveau de la classe

Certains d'entre vous se sont inquiétés de la possibilité d'appliquer une contrainte couvrant plusieurs propriétés ou d'exprimer une contrainte qui dépend de plusieurs propriétés. L'exemple classique est la validation d'adresse. Les adresses ont des règles complexes:

  • un nom de rue est quelque peu standard et doit certainement avoir une limite de longueur
  • la structure du code postal dépend entièrement du pays
  • la ville peut souvent être corrélée à un code postal et une vérification des erreurs peut être effectuée (à condition qu'un service de validation soit accessible)
  • en raison de ces interdépendances, une simple contrainte au niveau de la propriété correspond à la facture

La solution offerte par la spécification Bean Validation est double:

  • il offre la possibilité de forcer l'application d'un ensemble de contraintes avant un autre ensemble de contraintes grâce à l'utilisation de groupes et de séquences de groupes. Ce sujet sera traité dans la prochaine entrée de blog
  • il permet de définir des contraintes de niveau classe

Les contraintes au niveau de la classe sont des contraintes régulières (duo annotation/implémentation) qui s'appliquent à une classe plutôt qu'à une propriété. Autrement dit, les contraintes au niveau de la classe reçoivent l'instance d'objet (plutôt que la valeur de la propriété) dans 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;
        }
        // ...
    }
}

Les règles avancées de validation des adresses ont été exclues de l'objet adresse et implémentées par MultiCountryAddressValidator. En accédant à l'instance d'objet, les contraintes de niveau classe ont beaucoup de flexibilité et peuvent valider plusieurs propriétés corrélées. Notez que l'ordre n'est pas inclus dans l'équation ici, nous y reviendrons dans le prochain article.

Le groupe d'experts a discuté de différentes approches de prise en charge de plusieurs propriétés: nous pensons que l'approche de contrainte au niveau de la classe offre à la fois suffisamment de simplicité et de flexibilité par rapport à d'autres approches au niveau de la propriété impliquant des dépendances. Vos commentaires sont les bienvenus.

83
Pascal Thivent

Pour fonctionner correctement avec Bean Validation , l'exemple fourni dans Pascal Thivent answer pourrait être réécrit comme suit:

@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;
        }
    }
}
25
cassiomolin

Un validateur de niveau classe est la voie à suivre lorsque vous souhaitez rester avec la spécification de validation du bean. Si vous êtes heureux d'utiliser une fonctionnalité de validation Hibernate, vous pouvez utiliser @ ScriptAssert, qui est fourni dans Validator-4.1.0.Final.

8
Hardy