如何验证两个或更多字段的组合?

136

我正在使用JPA 2.0 / Hibernate验证来验证我的模型。现在我有一个情况需要验证两个字段的组合:

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

如果 getValue1()getValue2() 都为 null,那么该模型就是 无效的,否则就是有效的。

我如何使用 JPA 2.0/Hibernate 进行此类验证?使用简单的 @NotNull 注释时,两个getter都必须为非空才能通过验证。


2
可能是[使用Hibernate Validator(JSR 303)进行跨字段验证]的重复问题(https://dev59.com/rHI-5IYBdhLWcg3wMFW0) - Steve Chambers
5个回答

133

若要进行多个属性的验证,您应该使用类级别的约束。参考Bean Validation Sneak Peek part II: custom constraints

类级别约束

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

  • 街道名称通常比较标准,肯定有长度限制
  • 邮政编码结构完全取决于国家
  • 城市常常与邮政编码相关,可以进行一些错误检查(前提是有可访问的验证服务)
  • 由于这些相互依赖关系,简单的属性级别约束无法胜任

Bean Validation规范提供的解决方案有两个部分:

  • 通过使用组和组序列,在强制应用一组约束之前,可以强制应用另一组约束。该主题将在下一个博客条目中介绍。
  • 它允许定义类级别的约束

类级别的约束是常规的约束(注释/实现组合),它们适用于一个类而不是一个属性。换句话说,类级别的约束在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("@AddressAnnotation only applies to Address objects");
        }
        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被颠倒了。而isvalid()方法接受两个参数。 - Guillaume Husta
2
TYPERUNTIME应分别替换为ElementType.TYPERetentionPolicy.RUNTIME - mark.monteiro
3
您可以使用静态导入:import static java.lang.annotation.ElementType.*;import static java.lang.annotation.RetentionPolicy.*; - cassiomolin
2
我已经重写了这个示例,使其与Bean Validation一起使用。请点击此处查看。 - cassiomolin
1
在字段级别错误的情况下,如何显示错误消息 - Pradeep
显示剩余4条评论

54

为了正确使用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 存储在数据库的 Country 表中。从这里进行数据库调用是一个好主意吗?另外,我想在验证后将它们链接起来,因为 Address 属于 Country,我希望 address 条目具有 country 表的外键。我该如何将国家与地址关联起来? - krozaine
请注意,如果您在错误的对象上设置类型验证注释,则Bean Validation框架将抛出异常。例如,如果您在Country对象上设置@ValidAddress注释,则会收到“找不到约束'com.example.validation.ValidAddress'的验证器来验证类型'com.example.Country'”的异常。 - Jacob van Lingen

29

您可以像这样使用@javax.validation.constraints.AssertTrue验证:

public class MyModel {
    
    private String value1;
    private String value2;

    @AssertTrue(message = "Values are invalid")
    private boolean isValid() {
        return value1 != null || value2 != null;
    }
}

1
感谢您提供的解决方案。最初我想使用@ScriptAssert,但是有些bean需要验证很多规则,所以这种方法更直观:每个规则一个验证方法。 - t0r0X
2
谢谢,这比编写自定义的ConstraintValidator简单得多,而且需要的代码也更少。我认为对于这个简单的用例,应该优先考虑这种方法而不是已接受的解决方案。 - Balu
1
我认为应该选择这个。这种方法比其他方法更容易理解、记住和使用。 - undefined

13

如果您想遵循Bean验证规范,自定义类级别验证是可行的方式,例如这里提供了一个示例。

如果您愿意使用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 作为表达式语言而无需添加额外的依赖项。 - user41871
3
这是一个使用Hibernate Validator 5.1.1.Final创建类级别验证的示例。 - Ivan Hristov

0

编程语言:Java

这是一个帮助我的解决方案。

要求:

  1. 在UI上有一个表格,其中包含具有多个表/对象的映射关系的对象列表,并具有fk关系。

  2. 现在验证超出了多个fks,只有3个列不能重复。我的意思是3个的组合不能重复。

注意:由于我正在使用Java上的自定义框架,因此没有使用HashCode或equals的选项。如果我使用数组索引迭代,那么时间复杂度将增加,这是我不想要的。

解决方案:

我准备了一个字符串,它是一个自定义字符串,包含FK1的ID#FK2的ID#FK3的ID 例如:字符串将形成- > 1000L#3000L#1300L#

我们将使用set的add()将此字符串添加到集合中,如果出现重复,则返回false。

根据此标志,我们可以抛出验证消息。

这对我有所帮助。某些情况和限制可能不适用于DS。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接