如何结合验证两个或多个字段?


92

我正在使用JPA 2.0 /休眠验证来验证我的模型。我现在遇到一种情况,必须验证两个字段的组合:

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

该模型是无效的,如果都getValue1()getValue2()null和其他有效。

如何使用JPA 2.0 / Hibernate执行这种验证?使用简单的@NotNull注释,两个吸气剂都必须为非null才能通过验证。


Answers:


102

对于多属性验证,应使用类级别的约束。摘自 Bean Validation Sneak Peek第二部分:自定义约束

###类级别的约束

你们中的一些人对应用跨越多个属性的约束或表达依赖于多个属性的约束的能力表示担忧。经典示例是地址验证。地址具有复杂的规则:

  • 街道名称在某种程度上是标准名称,并且一定要有长度限制
  • 邮政编码结构完全取决于国家/地区
  • 该城市通常可以与邮政编码相关联,并且可以进行一些错误检查(前提是可以使用验证服务)
  • 由于这些相互依赖性,一个简单的属性级别约束确实可以满足要求

Bean验证规范提供的解决方案有两个方面:

  • 它提供了通过使用组和组序列来强制在一组其他约束之前应用一组约束的功能。下一个博客条目将涵盖该主题
  • 它允许定义类级别的约束

类级别约束是规则约束(注释/实现二重奏),适用于类而不是属性。换句话说,类级别的约束在中接收对象实例(而不是属性值)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;
        }
        // ...
    }
}

高级地址验证规则已被排除在地址对象之外,并由实现 MultiCountryAddressValidator。通过访问对象实例,类级别的约束具有很大的灵活性,并且可以验证多个相关属性。请注意,这里的排序不包括方程式,我们将在下一篇文章中再讨论。

专家组讨论了多种属性支持方法:与其他涉及依赖的属性级别方法相比,我们认为类级别约束方法既提供了足够的简便性又提供了灵活性。欢迎您提供反馈。


17
在示例中,接口ConstraintValidator和注解@Constraint已反转。并且valid()需要2个参数。
Guillaume

1
TYPE并且RUNTIME应分别用ElementType.TYPE和替换RetentionPolicy.RUNTIME
mark.monteiro '16

2
@ mark.monteiro您可以使用静态导入:import static java.lang.annotation.ElementType.*;import static java.lang.annotation.RetentionPolicy.*;
cassiomolin

2
我已经重写了该示例以使用Bean验证。在这里看看。
cassiomolin

1
注解的参数在规范内不正确,因为必须有消息,组和有效负载,如卡西欧在此答案中提到的那样。
Peter S.

37

为了与Bean Validation一起正常工作,可以将Pascal Thivent的答案中提供的示例重写如下:

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

如何在WebSphere Restful项目中为CDI bean引导或调用定制验证器?我已经写了所有东西,但是自定义约束不起作用或没有被调用
BalaajiChander

我陷入了类似的验证,但我isoA2Code存储在DBCountry表中。从这里进行数据库调用是一个好主意吗?另外,我想在验证后链接它们,因为Address belongs_toaCountry和我希望address条目具有country表的外键。我如何将国家与地址联系起来?
krozaine

请注意,当您在错误的对象上设置类型验证批注时,Bean验证框架将引发异常。例如,如果您要@ValidAddress在Country对象上设置注释,则会出现No validator could be found for constraint 'com.example.validation.ValidAddress' validating type 'com.example.Country'异常。
雅各布·范·林根

12

当您希望遵循Bean Validation规范时,可以使用自定义类级别验证器,例如此处

如果您乐于使用Hibernate Validator功能,则可以使用@ScriptAssert,它是Validator-4.1.0.Final之后提供的。摘自其JavaDoc:

脚本表达式可以用任何脚本语言或表达式语言编写,可以在类路径中找到与之兼容的JSR 223(“ JavaTM平台脚本”)引擎。

例:

@ScriptAssert(lang = "javascript", script = "_this.value1 != null || _this != value2)")
public class MyBean {
  private String value1;
  private String value2;
}

是的,Java 6包含Rhino(JavaScript引擎),因此您可以将JavaScript用作表达语言,而无需添加额外的依赖项。

3
是一个如何使用Hibernate Validator 5.1.1.Final创建这样的验证的示例
Ivan Hristov
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.