Jak mogę zweryfikować połączenie dwóch lub więcej pól?

92

Używam weryfikacji JPA 2.0 / Hibernate do sprawdzania poprawności moich modeli. Mam teraz sytuację, w której połączenie dwóch pól musi zostać sprawdzone:

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

Model jest nieważne , gdy oba getValue1()i getValue2()nulli ważne inaczej.

Jak mogę przeprowadzić tego rodzaju walidację za pomocą JPA 2.0 / Hibernate? W przypadku prostej @NotNulladnotacji oba metody pobierające muszą mieć wartość różną od null, aby przejść weryfikację.

Daniel Rikowski
źródło

Odpowiedzi:

102

W przypadku walidacji wielu właściwości należy użyć ograniczeń na poziomie klasy. Z Bean Validation Sneak Peek, część II: niestandardowe ograniczenia :

### Ograniczenia na poziomie klasy

Niektórzy z was wyrazili obawy co do możliwości zastosowania ograniczenia obejmującego wiele właściwości lub wyrażenia ograniczenia zależnego od kilku właściwości. Klasycznym przykładem jest walidacja adresu. Adresy mają skomplikowane zasady:

  • nazwa ulicy jest dość standardowa iz pewnością musi mieć ograniczenie długości
  • struktura kodu pocztowego całkowicie zależy od kraju
  • miasto często można skorelować z kodem pocztowym i można sprawdzić pewne błędy (pod warunkiem, że dostępna jest usługa walidacji)
  • ze względu na te współzależności proste ograniczenie na poziomie właściwości spełnia wymagania

Rozwiązanie oferowane przez specyfikację Bean Validation jest dwojakie:

  • oferuje możliwość wymuszenia zastosowania zestawu ograniczeń przed innym zestawem ograniczeń poprzez użycie grup i sekwencji grupowych. Temat ten zostanie omówiony w kolejnym wpisie na blogu
  • pozwala zdefiniować ograniczenia na poziomie klasy

Ograniczenia na poziomie klasy to zwykłe ograniczenia (duet adnotacji / implementacji), które mają zastosowanie do klasy, a nie do właściwości. Mówiąc inaczej, ograniczenia na poziomie klasy odbierają instancję obiektu (zamiast wartości właściwości) w formacie 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;
        }
        // ...
    }
}

Zaawansowane reguły sprawdzania poprawności adresu zostały pominięte w obiekcie adresu i zaimplementowane przez MultiCountryAddressValidator. Uzyskując dostęp do instancji obiektu, ograniczenia na poziomie klasy zapewniają dużą elastyczność i mogą weryfikować wiele skorelowanych właściwości. Zauważ, że porządkowanie nie jest tutaj uwzględnione, wrócimy do tego w następnym poście.

Grupa ekspertów omówiła różne podejścia do obsługi wielu właściwości: uważamy, że podejście z ograniczeniami na poziomie klasy zapewnia zarówno wystarczającą prostotę, jak i elastyczność w porównaniu z innymi podejściami na poziomie właściwości obejmującymi zależności. Twoja opinia jest mile widziana.

Pascal Thivent
źródło
17
Interfejs ConstraintValidator i adnotacja @Constraint zostały w przykładzie odwrócone. I jest poprawne () przyjmuje 2 parametry.
Guillaume Husta
1
TYPEi RUNTIMEpowinien być zastąpiony ElementType.TYPEi RetentionPolicy.RUNTIMEodpowiednio.
mark.monteiro
2
@ mark.monteiro Możesz użyć importu statycznego: import static java.lang.annotation.ElementType.*;iimport static java.lang.annotation.RetentionPolicy.*;
cassiomolin
2
Przepisałem przykład, aby działał z walidacją fasoli. Zajrzyj tutaj .
kasiomolina
1
Parametry adnotacji nie mieszczą się w specyfikacji, ponieważ musi istnieć komunikat, grupy i ładunek, jak wspomniano w tej odpowiedzi Cassio.
Peter S.
38

Aby pracować poprawnie z Bean Validation , na przykład przewidzianej w Pascalu Thivent za odpowiedź może zostać przepisany w następujący sposób:

@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;
        }
    }
}
kasiomolina
źródło
Jak uruchomić lub wywołać niestandardowy moduł sprawdzania poprawności w projekcie WebSphere Restful dla komponentu bean CDI? Napisałem wszystko, ale niestandardowe ograniczenie nie działa lub nie zostało wywołane
BalaajiChander
Utknąłem z podobną walidacją, ale moja isoA2Codejest przechowywana w Countrytabeli DB . Czy to dobry pomysł, aby wywołać DB stąd? Chciałbym również połączyć je po walidacji, ponieważ Address belongs_toa Countryi chcę, aby addresswpis zawierał countryklucz obcy tabeli. Jak powiązałbym kraj, z którym należy się zwrócić?
krozaine
Zauważ, że jeśli ustawisz adnotację walidacji typu na niewłaściwym obiekcie, wyjątek zostanie zgłoszony przez platformę sprawdzania poprawności Bean. Na przykład, jeśli ustawisz @ValidAddressadnotację w obiekcie Country, otrzymasz No validator could be found for constraint 'com.example.validation.ValidAddress' validating type 'com.example.Country'wyjątek.
Jacob van Lingen
12

Niestandardowy walidator na poziomie klasy jest dobrym rozwiązaniem, jeśli chcesz pozostać przy specyfikacji walidacji fasoli, na przykład tutaj .

Jeśli z przyjemnością korzystasz z funkcji Hibernate Validator, możesz użyć @ScriptAssert , który jest dostępny od wersji Validator-4.1.0.Final. Exceprt z JavaDoc:

Wyrażenia skryptowe można pisać w dowolnym języku skryptowym lub języku wyrażeń, dla którego w ścieżce klas można znaleźć silnik zgodny z JSR 223 („Scripting for the JavaTM Platform”).

Przykład:

@ScriptAssert(lang = "javascript", script = "_this.value1 != null || _this != value2)")
public class MyBean {
  private String value1;
  private String value2;
}
Wytrzymały
źródło
Tak, a Java 6 zawiera Rhino (silnik JavaScript), więc możesz używać JavaScript jako języka wyrażeń bez dodawania dodatkowych zależności.
3
Oto przykład, jak utworzyć taką walidację za pomocą Hibernate Validator 5.1.1.Final
Ivan Hristov