30
38

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Spring MVCにおけるフォームバリデーションの適用事例【後編】

Last updated at Posted at 2016-09-29

Spring MVCを利用したフォームバリデーションのカスタム事例の後編です。

前編はこちらです。
Spring MVCにおけるフォームバリデーションの適用事例【前編】

4. 相関チェックをバリデーターに実装する

相関チェックをフォームクラスではなく、バリデーターを作成して、そこでチェックする方法です。
フォーム内の項目に自由にアクセスできるため、自由度は格段に高まります。

ラジオボタンの選択内容によって、日付の入力要否が変わるチェックを実装してみます。
フォームクラスにはシンプルに入力項目だけ保持します。
(もちろん、制約を付けておけば、そのチェックも実施されます)

ValidationForm4.java
public class ValidationForm4 implements Serializable {
    // setter, getterは省略
    private String period;
    private Date dueDate;
}

入力チェックを実装するバリデーターを作成します。

Validator4.java
public class Validator4 implements Validator {

    @Override
    public boolean supports(Class<?> clazz) {
        return ValidationForm4.class.isAssignableFrom(clazz);
    }

    @Override
    public void validate(Object form, Errors errors) {
        ValidationForm4 validationForm4 = (ValidationForm4)form;
        String period = validationForm4.getPeriod();
        Date dueDate = validationForm4.getDueDate();
        // 期間のラジオボタン(A:当日、B:期間指定)でBが選択された場合は、日付の入力が必要
        if (period != null && period.equals("B")  && dueDate == null) {
            errors.rejectValue("dueDate", "dueDate.notnull");
        }
    }

}

validateメソッドで、フォームを受け取り、入力チェックを行います。
入力チェックエラーの場合は、rejectValueで、エラー項目とメッセージを設定します。

このバリデーターを呼び出すには、コントローラーでバリデーターをインジェクションし、@InitBinderで、バリデーターを登録します。
それだけで、フォームクラスの入力チェックのタイミングで、このバリデーターも呼ばれます。

ValidationController4.java
@Controller
public class ValidationController4 {

    @Autowired
    Validator4 validator4;
    
    @InitBinder
    public void validatorBinder(WebDataBinder binder) {
        binder.addValidators(validator4);
    }
    
    @InitBinder
    public void dateBinder(WebDataBinder binder) {
        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
        CustomDateEditor editor = new CustomDateEditor(dateFormat, true);
        binder.registerCustomEditor(Date.class, editor);
    }
    
    @ModelAttribute("radios")
    public Map<String, String> radioList() {
        return Collections.unmodifiableMap(new LinkedHashMap<String, String>() {
            {
                put("A", "当日");
                put("B", "期間指定");
            }
        });
    }
    
    @RequestMapping(value = "validation4", method = RequestMethod.GET)
    public String index(ValidationForm4 form, Model model) {
        return "validation4";
    }
    
    @RequestMapping(value = "validation4", method = RequestMethod.POST)
    public String confirm(@Validated ValidationForm4 form, BindingResult bindingResult, Model model) {
        // 入力チェックエラーがある場合は入力画面に戻る
        if (bindingResult.hasErrors()) {
            return "validation4";
        }
        // 入力チェックエラーがない場合は確認画面に遷移する
        model.addAttribute("validationForm4", form);
        //ラジオボタンの表示名を設定
        model.addAttribute("selectedPeriod", radioList().get(form.getPeriod())); 
        return "validationConfirm4";
    }

}

画面イメージはこちらです。

validation4e.png

こちらは、rejectValueで指定した項目に対してThymeleafのerrorclassが適用され、入力枠が赤くなってますね。

5. バリデーターからアノテーションを利用する

これまでのやりかたで、たいがいの入力チェックは実現できるのではないでしょうか?
他のちょっと工夫したやり方として、バリデーターから、条件に応じて適用する制約を変更してチェックを行う方法もあります。
できるだけ制約を利用することで、生産性アップや実装ミス防止につながります。

使用例として、以下の画面のように、受注を登録する画面があり、あらかじめ入力行をいくつか用意してあるとします。

validation5.png

入力チェックは以下のような仕様とします。

  • 商品コードが入力された行は、数量と単価が必須、数量と単価に対する桁数チェック、商品のマスタ存在チェックを行う
  • 商品コードが入力されていない行は、数量と単価は入力してはいけない

フォームクラスは、階層構造とします。

ValidationForm5.java
public class ValidationForm5 implements Serializable {
    // setter, getterは省略
    private List<ValidationForm5Child> validationForm5ChildList;
}

子の階層であるフォームクラスには、groupsを指定した制約を設定します。

ValidationForm5Child.java
public class ValidationForm5Child implements Serializable {
    // setter, getterは省略
    @ItemExists(groups = Selected.class)
    private String item; // 商品が入力された場合は@ItemExistsでマスタ存在チェック
    
    @NotNull(groups = Selected.class, message = "{qty.notnull}")
    @Digits(integer = 3, fraction = 0, groups = Selected.class, message = "{qty.digits}")
    @Null(groups = NotSelected.class, message = "{qty.null}")
    private Integer qty; // 入力対象行は@NotNullと@Digits、入力対象外行は@Null
    
    @NotNull(groups = Selected.class, message = "{price.notnull}")
    @Digits(integer = 3, fraction = 0, groups = Selected.class, message = "{price.digits}")
    @Null(groups = NotSelected.class, message = "{price.null}")
    private Integer price; // 入力対象行は@NotNullと@Digits、入力対象外行は@Null
}

groupsは、入力行としてSelected、入力対象外としてNotSelectedを表すクラスを作成します。
中身は不要です。

Selected.java
public @interface Selected {
}
NotSelected.java
public @interface NotSelected {
}

4と同様、入力チェックを実装するバリデーターを作成します。

Validator5.java
@Component
public class Validator5 implements Validator {

    @Autowired
    SmartValidator smartValidator;
    
    public boolean supports(Class<?> clazz) {
        return ValidationForm5.class.isAssignableFrom(clazz);
    }

    public void validate(Object form, Errors errors) {

        ValidationForm5 validationForm5 = (ValidationForm5)form;
        
        List<ValidationForm5Child> validationForm5ChildList = validationForm5.getValidationForm5ChildList();
        
        for(int i=0; i<validationForm5ChildList.size(); i++) {

            ValidationForm5Child validationForm5Child = validationForm5ChildList.get(i);
            String item = validationForm5Child.getItem();
            
            errors.pushNestedPath("validationForm5ChildList[" + i + "].");
            // 商品が入力されている場合は入力対象の行としてチェック
            if(item != null && !item.equals("")) {
                smartValidator.validate(validationForm5Child, errors, Selected.class);
            // 商品が入力されていない場合は入力対象外の行としてチェック
            } else {
                smartValidator.validate(validationForm5Child, errors,  NotSelected.class);
            }
            errors.popNestedPath();
        }

    }

}

ここでは、商品コードの有無に応じて、validateの引数であるgroupを変更します。

商品コードが入力されている行に対するチェック

smartValidator.validate(validationForm5Child, errors, Selected.class);

商品コードが入力されていない行に対するチェック

smartValidator.validate(validationForm5Child, errors,  NotSelected.class);

エラー対象のフィールドを正しく設定するため、validateメソッドを呼び出す前後で、

errors.pushNestedPath("validationForm5ChildList[" + i + "].");

errors.popNestedPath();

を実行します。

コントローラーも4と同様で、コントローラーでバリデーターをインジェクションし、@InitBinderで、バリデーターを登録します。
ちなみに、initFormメソッドで、入力行を初期化しています。

ValidationController5.java
@Controller
public class ValidationController5 {

    private static final Logger logger = LoggerFactory.getLogger(ValidationController5.class);
    
    @Autowired
    Validator5 validator5;
    
    @InitBinder
    public void validatorBinder(WebDataBinder binder) {
        binder.addValidators(validator5);
    }
    
    @ModelAttribute("validationForm5")
    public ValidationForm5 initForm() {
        ValidationForm5 form = new ValidationForm5();
        List<ValidationForm5Child> validationForm5ChildList = new ArrayList<ValidationForm5Child>();
        for(int i=0; i<5; i++) {
            validationForm5ChildList.add(new ValidationForm5Child());
        }
        form.setValidationForm5ChildList(validationForm5ChildList);
        return form;
    }
    
    @RequestMapping(value = "validation5", method = RequestMethod.GET)
    public String index(ValidationForm5 form, Model model) {
        return "validation5";
    }
    
    @RequestMapping(value = "validation5", method = RequestMethod.POST)
    public String confirm(@Validated ValidationForm5 form, BindingResult bindingResult, Model model) {
        // 入力チェックエラーがある場合は入力画面に戻る
        if (bindingResult.hasErrors()) {
            return "validation5";
        }
        // 入力チェックエラーがない場合は確認画面に遷移する
        model.addAttribute("validationForm5", form);
        return "validationConfirm5";
    }

}

HTMLでは入力行をテーブル形式で表示します。
最後の列はエラーメッセージ表示エリアです。

validation5.html
// 省略
<table>
    <tr>
        <th>No.</th>
        <th>商品</th>
        <th>数量</th>
        <th>単価</th>
        <th></th>
    </tr>
    <tr th:each="validationForm5ChildList, stat : *{validationForm5ChildList}">
        <td th:text="${stat.count}"></td>
        <td>
            <input type="text" th:field="*{validationForm5ChildList[__${stat.index}__].item}" th:errorclass="fieldError" />
        </td>
        <td>
            <input type="number" th:field="*{validationForm5ChildList[__${stat.index}__].qty}" th:errorclass="fieldError" />
        </td>
        <td>
            <input type="number" th:field="*{validationForm5ChildList[__${stat.index}__].price}" th:errorclass="fieldError" />
        </td>
        <td>
            <span th:if="${#fields.hasErrors('*{validationForm5ChildList[__${stat.index}__].*}')}" 
                th:errors="*{validationForm5ChildList[__${stat.index}__].*}" class="fieldErrorMessage"></span>
        </td>
    </tr>
</table>
// 省略

入力チェック実施後の画面イメージはこちらです。
入力対象の行と入力対象外の行で、適用されているチェックが異なることがわかります。

validation5e.png

さいごに

画面や入力項目が多い業務システムでは、入力チェックも多く、チェック仕様も複雑になりがちです。
ただ、同じ業務システムでは、画面横断的に使用する入力項目は似てきます。(商品、社員、顧客、数量、金額など)
それらの項目は事前に制約を作成しておくと、実装も1ヶ所にまとめられ、ミスもなくせ、何より生産性がアップします。
今回、いろいろなフォームバリデーションを試してみて、改めてBean Validation APIの便利さと奥深さを知ることができました。

30
38
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
30
38

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?