Wie kann ich zwei oder mehr Felder in Kombination validieren?

89

Ich verwende die JPA 2.0 / Hibernate-Validierung, um meine Modelle zu validieren. Ich habe jetzt eine Situation, in der die Kombination von zwei Feldern validiert werden muss:

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

Das Modell ist ungültig , wenn beide getValue1()und getValue2()sind nullund gültig anders.

Wie kann ich diese Art der Validierung mit JPA 2.0 / Hibernate durchführen? Mit einer einfachen @NotNullAnmerkung müssen beide Getter ungleich Null sein, um die Validierung zu bestehen.

Daniel Rikowski
quelle
2
Mögliches Duplikat der Cross-Field-Validierung mit Hibernate Validator (JSR 303)
Steve Chambers

Antworten:

101

Für die Überprüfung mehrerer Eigenschaften sollten Sie Einschränkungen auf Klassenebene verwenden. Aus Bean Validation Sneak Peek Teil II: Benutzerdefinierte Einschränkungen :

Einschränkungen auf Klassenebene

Einige von Ihnen haben Bedenken hinsichtlich der Möglichkeit geäußert, eine Einschränkung anzuwenden, die mehrere Eigenschaften umfasst, oder Einschränkungen auszudrücken, die von mehreren Eigenschaften abhängen. Das klassische Beispiel ist die Adressvalidierung. Adressen haben komplizierte Regeln:

  • Ein Straßenname ist etwas Standard und muss auf jeden Fall eine Längenbeschränkung haben
  • Die Struktur der Postleitzahl hängt vollständig vom Land ab
  • Die Stadt kann häufig mit einer Postleitzahl korreliert werden, und es kann eine Fehlerprüfung durchgeführt werden (vorausgesetzt, ein Validierungsdienst ist verfügbar).
  • Aufgrund dieser Abhängigkeiten passt eine einfache Einschränkung auf Eigenschaftsebene zur Rechnung

Die Bean Validation-Spezifikation bietet zwei Lösungen:

  • Es bietet die Möglichkeit, die Verwendung einer Reihe von Einschränkungen vor einer anderen Reihe von Einschränkungen durch die Verwendung von Gruppen und Gruppensequenzen zu erzwingen. Dieses Thema wird im nächsten Blogeintrag behandelt
  • Es ermöglicht das Definieren von Einschränkungen auf Klassenebene

Einschränkungen auf Klassenebene sind reguläre Einschränkungen (Annotation / Implementation Duo), die für eine Klasse und nicht für eine Eigenschaft gelten. Anders gesagt, Einschränkungen auf Klassenebene erhalten die Objektinstanz (anstelle des Eigenschaftswerts) in 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;
        }
        // ...
    }
}

Die erweiterten Adressvalidierungsregeln wurden aus dem Adressobjekt herausgelassen und von implementiert MultiCountryAddressValidator. Durch den Zugriff auf die Objektinstanz sind Einschränkungen auf Klassenebene sehr flexibel und können mehrere korrelierte Eigenschaften validieren. Beachten Sie, dass die Reihenfolge hier nicht in der Gleichung enthalten ist. Wir werden im nächsten Beitrag darauf zurückkommen.

Die Expertengruppe hat verschiedene Ansätze zur Unterstützung mehrerer Eigenschaften erörtert: Wir glauben, dass der Constraint-Ansatz auf Klassenebene im Vergleich zu anderen Ansätzen auf Eigenschaftsebene, die Abhängigkeiten beinhalten, sowohl eine ausreichende Einfachheit als auch Flexibilität bietet. Ihr Feedback ist willkommen.

Pascal Thivent
quelle
17
Die Schnittstelle ConstraintValidator und die Annotation @Constraint wurden im Beispiel invertiert. Und ist gültig () nimmt 2 Parameter.
Guillaume Husta
1
TYPEund RUNTIMEsollte ersetzt werden ElementType.TYPEund RetentionPolicy.RUNTIMEsind.
mark.monteiro
2
@ mark.monteiro Sie können statische Importe verwenden: import static java.lang.annotation.ElementType.*;undimport static java.lang.annotation.RetentionPolicy.*;
Cassiomolin
2
Ich habe das Beispiel umgeschrieben, um mit Bean Validation zu arbeiten. Schauen Sie hier .
Cassiomolin
1
Die Parameter der Annotation liegen nicht innerhalb der Spezifikation korrekt, da es eine Nachricht, Gruppen und Nutzdaten geben muss, wie sie von Cassio unter dieser Antwort erwähnt wurden.
Peter S.
34

Um mit Bean Validation ordnungsgemäß zu arbeiten , könnte das in der Antwort von Pascal Thivent bereitgestellte Beispiel wie folgt umgeschrieben werden:

@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;
        }
    }
}
Cassiomolin
quelle
Wie kann ich einen benutzerdefinierten Validator in einem WebSphere Restful-Projekt für eine CDI-Bean booten oder aufrufen? Ich habe alles geschrieben, aber die benutzerdefinierte Einschränkung funktioniert nicht oder wird nicht aufgerufen
BalaajiChander
Ich bin mit einer ähnlichen Validierung festgefahren, aber meine isoA2Codeist in der DB- CountryTabelle gespeichert . Ist es eine gute Idee, von hier aus einen DB-Aufruf zu tätigen? Außerdem möchte ich sie nach der Validierung verknüpfen, da Address belongs_toa Countryund ich möchten, dass der addressEintrag countryden Fremdschlüssel der Tabelle enthält. Wie würde ich das Land mit der Adresse verknüpfen?
Krozaine
Beachten Sie, dass das Bean Validation-Framework eine Ausnahme auslöst, wenn Sie eine Annotation zur Typüberprüfung für ein falsches Objekt festlegen. Wenn Sie beispielsweise die @ValidAddressAnmerkung für das Country-Objekt festlegen , wird eine No validator could be found for constraint 'com.example.validation.ValidAddress' validating type 'com.example.Country'Ausnahme angezeigt.
Jacob van Lingen
10

Ein benutzerdefinierter Validator auf Klassenebene ist der richtige Weg, wenn Sie die Bean Validation-Spezifikation beibehalten möchten (Beispiel hier) .

Wenn Sie gerne eine Hibernate Validator-Funktion verwenden, können Sie @ScriptAssert verwenden , das seit Validator-4.1.0.Final bereitgestellt wird. Ausgenommen von seinem JavaDoc:

Skriptausdrücke können in jeder Skript- oder Ausdruckssprache geschrieben werden, für die eine JSR 223- kompatible Engine ("Scripting für die JavaTM-Plattform") im Klassenpfad gefunden werden kann.

Beispiel:

@ScriptAssert(lang = "javascript", script = "_this.value1 != null || _this != value2)")
public class MyBean {
  private String value1;
  private String value2;
}
Winterhart
quelle
Ja, und Java 6 enthält Rhino (JavaScript-Engine), sodass Sie JavaScript als Ausdruckssprache verwenden können, ohne zusätzliche Abhängigkeiten hinzuzufügen.
3
Hier ist ein Beispiel, wie man eine solche Validierung mit Hibernate Validator 5.1.1.Final
Ivan Hristov