Bean ValidationのGroup sequenceは単項目チェック、相関チェックの順序指定で使うのは止めた方が良さそう

## Bean Validationの相関チェックについて

これはVaidation Nightの補足記事。
http://www.slideshare.net/eiryu/javabean-validation

Validation Nightで「Bean Validationの相関チェックは、メソッドに@ AssertTrueつけて書く」と言った。
その際に、バリデーションの順番がランダムなため、単項目チェックより相関チェックが先に実行されることを想定してコードを書く必要があるということにも言及した。

具体的には以下のような感じ。

```java
public class UserForm {
    // 略

	@javax.validation.constraints.NotNull(message = "{NotNull.sex}")
    private Sex sex;

    private Boolean pregnant;

    public UserForm() {
    }

    @javax.validation.constraints.AssertTrue(message = "{AssertTrue.sexAndPregnant}")
    public boolean isValidPregnant() {

        // 性別が入力されていない時は、そちらで引っかかるのでバリデーションしない
        if (sex == null) {
            return true;
        }

        // 妊娠していると選択している人が女性であることをチェック
        if (pregnant) {
            return Sex.FEMALE == sex;
        }
        return true;
    }
    
    // 略
}
```

上記コードの、

```java
        // 性別が入力されていない時は、そちらで引っかかるのでバリデーションしない
        if (sex == null) {
            return true;
        }
```

の部分については、本来のバリデーションとは関係のないコードである。
意味合いとしては、単項目チェックがOKになって初めて相関チェックをすべきなので、相関チェック自体はバリデーションOKにしとくという感じ。上記コードがないと、性別を入力していなくて妊娠している、という入力があった場合に相関チェックでバリデーションエラーになってしまう。
しかも、相関チェックに関係するフィールドが増えれば増えるほど、上記のような相関チェックをしないために相関チェックをOKにするコードを書く必要がある。Bean Validation版ボイラープレートコードと言ったところか。


## Group sequenceでバリデーションの順序を指定するのはどうか？

スライド中でも少し紹介したが、Group sequenceを使うとバリデーションの順序を指定出来る。だが、それを使っても一部問題があるのだが後述。

まず、バリデーションの順序を定義する。
単項目チェック、相関チェックという順序でバリデーションを行うように定義するには以下のようにする。

```java
/**
 * 単項目チェックgroup
 */
public interface SingleCheck {
}

/**
 * 相関チェックgroup
 */
public interface CorrelatedCheck {
}

/**
 * チェックの順序を示す
 */
@javax.validation.GroupSequence({SingleCheck.class, CorrelatedCheck.class})
public interface CheckSequence {
}
```

単項目チェック、相関チェックのgroupはインタフェースとして定義。その順序を示すインタフェースを作成して、順序は@ javax.validation.GroupSequenceで定義。

そして以下のようなFormがあるとする。

- コンピュータを持っているか
- モバイルデバイスを持っているか
- 持っているコンピュータ
- 持っているモバイルデバイス

チェックする内容は以下。

- 単項目チェック
	- 「コンピュータを持っているか」が入力されていること
	- 「モバイルデバイスを持っているか」が入力されていること

- 相関チェック
	- 「コンピュータを持っているか」が入力されている場合は、「持っているコンピュータ」が入力されていること
	- 「コンピュータを持っているか」が未入力の場合は、「持っているコンピュータ」が未入力であること
	- 「モバイルデバイスを持っているか」が入力されている場合は、「持っているモバイルデバイス」が入力されていること
	- 「モバイルデバイスを持っているか」が未入力の場合は、「持っているモバイルデバイス」が未入力であること


```java
public class Form {

    @javax.validation.constraints.NotNull(groups = SingleCheck.class)
    private Boolean hasComputer;

    @javax.validation.constraints.NotNull(groups = SingleCheck.class)
    private Boolean hasMobileDevices;

    private Computer computer;
    private MobileDevice mobileDevice;

    public Form() {
    }

    @javax.validation.constraints.AssertTrue(groups = CorrelatedCheck.class)
    public boolean isValidComputer() {
        return hasComputer ? (computer != null) : (computer == null);
    }

    @javax.validation.constraints.AssertTrue(groups = CorrelatedCheck.class)
    public boolean isValidMobileDevice() {
        return hasMobileDevices ? (mobileDevice != null) : (mobileDevice == null);
    }
    
    // 略
}

public enum Computer {
    WINDOWS, MAC, LINUX;
}

public enum MobileDevice {
    IOS, ANDROID;
}
```

単項目チェック、相関チェックについては＠ NotNullなどのConstraintsのgroupsで指定。

この状態で以下のような入力をしてみる。

- 「コンピュータを持っているか」がtrue
- 「モバイルデバイスを持っているか」は未入力（null）
- 「持っているコンピュータ」は「MAC」を指定
- 「持っているモバイルデバイス」は「IOS」を指定

テストコードは以下。

```java
import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.Validator;
import java.util.Set;

public class FormTest {

    private static final Logger LOGGER = LoggerFactory.getLogger(FormTest.class);

    private Validator validator = Validation.buildDefaultValidatorFactory().getValidator();

    @Test
    public void validate_単項目チェックでエラー() {
        Form form = new Form();
        form.setHasComputer(true);
//        form.setHasMobileDevices(true);
        form.setComputer(Computer.MAC);
        form.setMobileDevice(MobileDevice.IOS);

        // 単項目チェックでエラーになるため、相関チェックは一切行われない
        Set<ConstraintViolation<Form>> violations = validator.validate(form, CheckSequence.class);
        LOGGER.info("violations: " + violations);
    }
}
```

コメントにも書いてあるが、上記の入力をバリデーションすると、単項目チェックまでしか行われない。
理由は、「モバイルデバイスを持っているか」が単項目チェックでバリデーションエラーになるから。

これは、Bean ValidationのGroup sequenceの仕様。
指定してあるバリデーションの順序の途中でバリデーションエラーが発生した場合は、それ以降のバリデーションは一切行われない。

普通の要件では、相関チェックまで出来るものは行い、単項目チェックまでしか出来ないものはそこで止めておく、というのが圧倒的に多いと思う。でないと、画面上で出ているエラーを解消してもまた別のエラーが出る、というような作りになってしまう。

このような事情があるため、Group sequenceを使わないで相関チェック内でBean Validation版ボイラープレートコードを書いてでも@ AssertTrueで相関チェックをする方が楽だと思う。


### ソース

https://github.com/eiryu/validation-night