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

92

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 n'est pas valide si les deux getValue1()et getValue2()sont nullet valide dans le cas contraire.

Comment puis-je effectuer ce type de validation avec JPA 2.0 / Hibernate? Avec une simple @NotNullannotation, les deux getters doivent être non nuls pour réussir la validation.

Daniel Rikowski
la source
2
duplication possible de la validation de champ croisé avec Hibernate Validator (JSR 303)
Steve Chambers

Réponses:

102

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

### Contraintes au niveau de la classe

Certains d'entre vous ont exprimé des inquiétudes quant à la capacité 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 de niveau de propriété fait pour s'adapter à la facture

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

  • il offre la possibilité de forcer un ensemble de contraintes à appliquer 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 au niveau de la 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é. En d'autres termes, les contraintes au niveau de la classe reçoivent l'instance de l'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 d'adresse ont été laissées en dehors de l'objet d'adresse et implémentées par MultiCountryAddressValidator. En accédant à l'instance d'objet, les contraintes au niveau de la classe ont beaucoup de flexibilité et peuvent valider plusieurs propriétés corrélées. Notez que la commande est exclue de l'équation ici, nous y reviendrons dans le prochain article.

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

Pascal Thivent
la source
17
L'interface ConstraintValidator et l'annotation @Constraint ont été inversées dans l'exemple. Et est valide () prend 2 paramètres.
Guillaume Husta
1
TYPEet RUNTIMEdoit être remplacé par ElementType.TYPEet RetentionPolicy.RUNTIME, respectivement.
mark.monteiro
2
@ mark.monteiro Vous pouvez utiliser des importations statiques: import static java.lang.annotation.ElementType.*;etimport static java.lang.annotation.RetentionPolicy.*;
cassiomoline
2
J'ai réécrit l'exemple pour travailler avec Bean Validation. Jetez un oeil ici .
cassiomolin
1
Les paramètres d'annotation ne sont pas corrects dans les spécifications, car il doit y avoir un message, des groupes et une charge utile comme cela a été mentionné par Cassio sous cette réponse.
Peter S.
38

Pour fonctionner correctement avec Bean Validation , l'exemple fourni dans la réponse de Pascal Thivent 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;
        }
    }
}
cassiomoline
la source
Comment amorcer ou appeler un validateur personnalisé dans un projet WebSphere restful pour un bean CDI? J'ai tout écrit, mais la contrainte personnalisée ne fonctionne pas ou est invoquée
BalaajiChander
Je suis coincé avec une validation similaire, mais mon isoA2Codeest stocké dans la Countrytable DB . Est-ce une bonne idée de faire un appel DB à partir d'ici? Aussi, je voudrais les lier après validation car Address belongs_toa Countryet je veux que l' addressentrée ait countryla clé étrangère de la table. Comment lier le pays à l'adresse?
krozaine
Notez que lorsque vous définissez une annotation de validation de type sur un objet incorrect, une exception sera levée par le framework Bean Validation. Par exemple, si vous définissez l' @ValidAddressannotation sur l'objet Country, vous obtiendrez une No validator could be found for constraint 'com.example.validation.ValidAddress' validating type 'com.example.Country'exception.
Jacob van Lingen le
12

Un validateur de niveau de classe personnalisé est la voie à suivre, lorsque vous souhaitez rester avec la spécification Bean Validation, exemple ici .

Si vous êtes satisfait d'utiliser une fonctionnalité Hibernate Validator, vous pouvez utiliser @ScriptAssert , qui est fourni depuis Validator-4.1.0.Final. Exceprt de son JavaDoc:

Les expressions de script peuvent être écrites dans n'importe quel langage de script ou d'expression, pour lequel un moteur compatible JSR 223 ("Scripting for the JavaTM Platform") se trouve sur le chemin de classe.

Exemple:

@ScriptAssert(lang = "javascript", script = "_this.value1 != null || _this != value2)")
public class MyBean {
  private String value1;
  private String value2;
}
Robuste
la source
Oui, et Java 6 inclut Rhino (moteur JavaScript) afin que vous puissiez utiliser JavaScript comme langage d'expression sans ajouter de dépendances supplémentaires.
3
Voici un exemple de création d'une telle validation avec Hibernate Validator 5.1.1.Final
Ivan Hristov