1. 序章

Java Bean Validation Basics チュートリアルでは、さまざまな組み込みのjavax.validation制約の使用法を確認しました。 このチュートリアルでは、javax.validation制約をグループ化する方法を説明します。

2. 使用事例

Beanの特定のフィールドのセットに制約を適用する必要があり、後で同じBeanの別のフィールドのセットに制約を適用する必要があるシナリオはたくさんあります。

たとえば、2段階の申し込みフォームがあるとします。 最初のステップでは、名前、名前、メールID、電話番号、キャプチャなどの基本情報をユーザーに提供するように求めます。 ユーザーがこのデータを送信するときは、この情報のみを検証する必要があります。

次のステップでは、アドレスなどの他の情報を提供するようにユーザーに依頼します。この情報も検証する必要があります。キャプチャは両方のステップに存在することに注意してください。

3. 検証制約のグループ化

すべてのjavax検証制約には、 groups という名前の属性があります。要素に制約を追加すると、そのグループの名前を宣言できます。制約は属します。これは、制約のgroups属性でグループインターフェイスのクラス名を指定することによって行われます。

何かを理解する最良の方法は、手を汚すことです。 javax制約をグループに組み合わせる方法を実際に見てみましょう。

3.1. 制約グループの宣言

最初のステップは、いくつかのインターフェースを作成することです。 これらのインターフェースは、制約グループ名になります。 このユースケースでは、検証制約を2つのグループに分けています。

最初の制約グループBasicInfoを見てみましょう。

public interface BasicInfo {
}

次の制約グループはAdvanceInfoです。

public interface AdvanceInfo {
}

3.2. 制約グループの使用

制約グループを宣言したので、 RegistrationFormJavaBeanでそれらを使用します。

public class RegistrationForm {
    @NotBlank(groups = BasicInfo.class)
    private String firstName;
    @NotBlank(groups = BasicInfo.class)
    private String lastName;
    @Email(groups = BasicInfo.class)
    private String email;
    @NotBlank(groups = BasicInfo.class)
    private String phone;

    @NotBlank(groups = {BasicInfo.class, AdvanceInfo.class})
    private String captcha;

    @NotBlank(groups = AdvanceInfo.class)
    private String street;
    
    @NotBlank(groups = AdvanceInfo.class)
    private String houseNumber;
    
    @NotBlank(groups = AdvanceInfo.class)
    private String zipCode;
    
    @NotBlank(groups = AdvanceInfo.class)
    private String city;
    
    @NotBlank(groups = AdvanceInfo.class)
    private String contry;
}

制約groups属性を使用して、ユースケースに応じてBeanのフィールドを2つのグループに分割しました。 デフォルトでは、すべての制約がデフォルトの制約グループに含まれています。

3.3. 1つのグループを持つ制約のテスト

制約グループを宣言し、それらをBeanクラスで使用したので、次はこれらの制約グループの動作を確認します。

まず、検証に BasicInfo 制約グループを使用して、基本情報が完全でない場合を確認します。 フィールドの@NotBlank制約のgroups属性でBasicInfo.classを使用した場合、空白のままにしたフィールドに対して制約違反が発生するはずです。

public class RegistrationFormUnitTest {
    private static Validator validator;

    @BeforeClass
    public static void setupValidatorInstance() {
        validator = Validation.buildDefaultValidatorFactory().getValidator();
    }

    @Test
    public void whenBasicInfoIsNotComplete_thenShouldGiveConstraintViolationsOnlyForBasicInfo() {
        RegistrationForm form = buildRegistrationFormWithBasicInfo();
        form.setFirstName("");
 
        Set<ConstraintViolation<RegistrationForm>> violations = validator.validate(form, BasicInfo.class);
 
        assertThat(violations.size()).isEqualTo(1);
        violations.forEach(action -> {
            assertThat(action.getMessage()).isEqualTo("must not be blank");
            assertThat(action.getPropertyPath().toString()).isEqualTo("firstName");
        });
    }

    private RegistrationForm buildRegistrationFormWithBasicInfo() {
        RegistrationForm form = new RegistrationForm();
        form.setFirstName("devender");
        form.setLastName("kumar");
        form.setEmail("[email protected]");
        form.setPhone("12345");
        form.setCaptcha("Y2HAhU5T");
        return form;
    }
 
    //... additional tests
}

次のシナリオでは、検証に AdvanceInfo 制約グループを使用して、高度な情報が不完全であるかどうかを確認します。

@Test
public void whenAdvanceInfoIsNotComplete_thenShouldGiveConstraintViolationsOnlyForAdvanceInfo() {
    RegistrationForm form = buildRegistrationFormWithAdvanceInfo();
    form.setZipCode("");
 
    Set<ConstraintViolation<RegistrationForm>> violations = validator.validate(form, AdvanceInfo.class);
 
    assertThat(violations.size()).isEqualTo(1);
    violations.forEach(action -> {
        assertThat(action.getMessage()).isEqualTo("must not be blank");
        assertThat(action.getPropertyPath().toString()).isEqualTo("zipCode");
    });
}

private RegistrationForm buildRegistrationFormWithAdvanceInfo() {
    RegistrationForm form = new RegistrationForm();
    return populateAdvanceInfo(form);
}

private RegistrationForm populateAdvanceInfo(RegistrationForm form) {
    form.setCity("Berlin");
    form.setContry("DE");
    form.setStreet("alexa str.");
    form.setZipCode("19923");
    form.setHouseNumber("2a");
    form.setCaptcha("Y2HAhU5T");
    return form;
}

3.4. 複数のグループを持つ制約のテスト

制約に複数のグループを指定できます。 このユースケースでは、基本情報と高度な情報の両方でcaptchaを使用しています。 まず、captchaBasicInfoでテストしてみましょう。

@Test
public void whenCaptchaIsBlank_thenShouldGiveConstraintViolationsForBasicInfo() {
    RegistrationForm form = buildRegistrationFormWithBasicInfo();
    form.setCaptcha("");
 
    Set<ConstraintViolation<RegistrationForm>> violations = validator.validate(form, BasicInfo.class);
 
    assertThat(violations.size()).isEqualTo(1);
    violations.forEach(action -> {
        assertThat(action.getMessage()).isEqualTo("must not be blank");
        assertThat(action.getPropertyPath().toString()).isEqualTo("captcha");
    });
}

次に、captchaAdvanceInfoでテストしてみましょう。

@Test
public void whenCaptchaIsBlank_thenShouldGiveConstraintViolationsForAdvanceInfo() {
    RegistrationForm form = buildRegistrationFormWithAdvanceInfo();
    form.setCaptcha("");
 
    Set<ConstraintViolation<RegistrationForm>> violations = validator.validate(form, AdvanceInfo.class);
 
    assertThat(violations.size()).isEqualTo(1);
    violations.forEach(action -> {
        assertThat(action.getMessage()).isEqualTo("must not be blank");
        assertThat(action.getPropertyPath().toString()).isEqualTo("captcha");
    });
}

4. GroupSequenceを使用した制約グループの検証順序の指定

デフォルトでは、制約グループは特定の順序で評価されません。 ただし、一部のグループを他のグループよりも先に検証する必要があるユースケースがある場合があります。 これを達成するために、 GroupSequenceを使用してグループ検証の順序を指定します。 

GroupSequenceアノテーションを使用する方法は2つあります。

  • 検証されるエンティティについて
  • インターフェイス

4.1. 検証中のエンティティでGroupSequenceを使用する

これは、制約を並べ替える簡単な方法です。 GroupSequence でエンティティに注釈を付け、制約の順序を指定しましょう。

@GroupSequence({BasicInfo.class, AdvanceInfo.class})
public class RegistrationForm {
    @NotBlank(groups = BasicInfo.class)
    private String firstName;
    @NotBlank(groups = AdvanceInfo.class)
    private String street;
}

4.2. インターフェイスでのGroupSequenceの使用

インターフェイスを使用して制約検証の順序を指定することもできます。 このアプローチの利点は、同じシーケンスを他のエンティティに使用できることです。 上記で定義したインターフェースでGroupSequenceを使用する方法を見てみましょう。

@GroupSequence({BasicInfo.class, AdvanceInfo.class})
public interface CompleteInfo {
}

4.3. GroupSequenceのテスト

では、テストしてみましょう GroupSequence。 まず、次の場合にテストします BasicInfo 不完全である場合、 AdvanceInfo グループ制約は評価されません:

@Test
public void whenBasicInfoIsNotComplete_thenShouldGiveConstraintViolationsForBasicInfoOnly() {
    RegistrationForm form = buildRegistrationFormWithBasicInfo();
    form.setFirstName("");
 
    Set<ConstraintViolation<RegistrationForm>> violations = validator.validate(form, CompleteInfo.class);
 
    assertThat(violations.size()).isEqualTo(1);
    violations.forEach(action -> {
        assertThat(action.getMessage()).isEqualTo("must not be blank");
        assertThat(action.getPropertyPath().toString()).isEqualTo("firstName");
    });
}

次に、 BasicInfo が完了したら、AdvanceInfo制約を評価する必要があることをテストします。

@Test
public void whenBasicAndAdvanceInfoIsComplete_thenShouldNotGiveConstraintViolationsWithCompleteInfoValidationGroup() {
    RegistrationForm form = buildRegistrationFormWithBasicAndAdvanceInfo();
 
    Set<ConstraintViolation<RegistrationForm>> violations = validator.validate(form, CompleteInfo.class);
 
    assertThat(violations.size()).isEqualTo(0);
}

5. 結論

このクイックチュートリアルでは、javax.validation制約をグループ化する方法を説明しました。

いつものように、すべてのコードスニペットはGitHub利用できます。