アレについて記す

Spring Boot(MVC)のFormにてネストしたオブジェクトを扱う

Posted on February 04, 2015 at 20:09 (JST)

Spring Boot(MVC)のFormにて、ネストしたオブジェクトを扱う方法を記載します。
業務システムの明細入力画面やECサイトの商品数量入力画面など、行数を可変としたいケースが多々あります。
そのような場面ではFormオブジェクト(※1)にListを持たせ、行ごとの入力値を取り扱うのが常套手段ですよね。
Spring Boot(MVC)ではRequestからFormオブジェクトへのマッピング、およびList内のValidationもしっかりとサポートしています。

作成したサンプルはGithubにて公開しています。[ simple-mvc-app ]

今回はECサイトっぽく、[商品数量入力]→[入力内容確認]→[完了]画面を作ってみました。
ポイントとなるのは入力→エラーチェック→入力画面での入力状態復元+エラーメッセージ表示といった部分です。
ソースの内容を理解しやすくするよう、画面キャプチャを貼ります。
入力に不備あり
エラー表示
確認画面
入力→エラー後表示→(入力値修正)→確認画面といったシナリオです。
 ※ サンプルなので、いろんな機能削ぎ落として最低限+αとなっています。念のため。。。

ソース

まずはJavaのソースを見ていきましょう。親Formオブジェクトから。

[ PurchaseForm.java(抜粋)]

public class PurchaseForm {

    @Valid
    private List<ItemForm> items;

    <他は今回のポイントではないので省略>
}

@Validを付与することで、Listに保持したオブジェクトもvalidationの対象となります。

[ ItemForm.java(抜粋)]

public class ItemForm {

    private Long itemId;

    private String itemName;

    private Integer price;

    @Max(99)
    @Min(0)
    private Integer quantity = 1;

    public Boolean isLater;

    <以下、省略>
}

Listに保持したオブジェクトも、親クラスと同様の指定でチェックを行うことが出来ます。

続いてControllerを見てみましょう。

[ PurchaseController.java(抜粋)]

@RequestMapping(value="create", method = RequestMethod.GET)
public String create(Model model) {

    List<Production> productions = initProductions(); // 画面表示用の情報を用意しているだけなので省略
    model.addAttribute("productions", productions);

    model.addAttribute("paymentMethods", Arrays.asList(PaymentMethod.values()));
    model.addAttribute("prefectures", Arrays.asList(Prefecture.values()));
    return "purchase/create";
}

@RequestMapping(value="confirm", method = RequestMethod.POST)
public String confirm(@Validated PurchaseForm form, BindingResult result, Locale locale, Model model) {

    if (result.hasErrors()) { return create(model); }

    // 実際は在庫チェックとかいろいろやることあるけど省略

    Integer totalPrice = form.getItems().stream()
            .mapToInt(item -> item.getPrice() * item.getQuantity())
            .sum();

    model.addAttribute("totalPrice", totalPrice);
    model.addAttribute("purchaseForm", form);

    return "purchase/confirm";
}

#confirmにて入力チェックを行っています。ネストしたオブジェクト用にチェック処理を別途記載する必要はありません。
もちろん、RequestからFormオブジェクトへの詰め替えもフレームワーク内で自動で行ってくれます。

最後にHTMLを見てみましょう。

[ purchase/create.html(抜粋)]

<div layout:fragment="content">
<h1>商品購入</h1>
<div class="col-sm-12">
  <form <b>th:action="@{/purchase/confirm}" th:object="${purchaseForm}"</b> class="form" method="post">
    <fieldset>
      <legend>ご購入商品</legend>
      <table>
        <tbody>
          <span <b>th:each="production, stat : ${productions}"</b>>
            <tr>

              <td class="form-group" <b>th:classappend="${#fields.hasErrors('items[' + stat.index + '].quantity')}? 'has-error has-feedback'"</b>>
                <input type="number" class="quantities form-control" th:id="${'quantity_' + stat.index}"
                    <b>th:name="${'items[' + stat.index + '].quantity'}"</b>
                    <b>th:field="*{items[__${stat.index}__].quantity}"</b>
                       th:readonly="${purchaseForm?.items?.size() ge stat.count} ? ${purchaseForm?.items?.get(stat.index)?.isLater} : false"/>
                <span <b>th:if="${#fields.hasErrors('items[' + stat.index + '].quantity')}"
                    th:errors="*{items[__${stat.index}__].quantity}"</b> class="help-block">error!</span>
              </td>

            </tr>
          </span>
        </tbody>
      </table>

入力画面の初期表示では、まずProductionのリストをもとにテーブルを出力しています。
そして、各行にテキストボックスを配備しています。

RequestとFormオブジェクトをマッピングするには、name属性を正しく指定する必要があります。
PucheaseForm.itemsに値をマッピングするには下記のように記述します。
例)name="items[0].hogehoge"
サンプルでは、[0]の数値を可変とするため、stat.indexを使用しています。

最後に、エラー時の表示について説明します。
エラーとなったかどうかは、name属性をキーに判定し、エラーメッセージの取り出しはONGL式をキーに行います。
${...}を使用する際は通常のプロパティアクセス方法にて行い、*{...}を使用する場合はONGL式で指定します。

目的に応じた値をキーに処理を行う必要があるため、慣れるまで少し大変ですね。
それでも、ネスト構造のマッピング・validationを特に意識せずに行えるのは嬉しいですよね。

  • ※1 SpringMVCにはStrutsのようなForm用オブジェクトは存在しません。
    本記事では便宜上Formの値を格納するクラスをFormオブジェクトと読んでいます。

以上です。