LoginSignup
100
83

More than 5 years have passed since last update.

Thymeleafのth:fieldの意味とSpring Frameworkの値の受け渡しの仕組み

Last updated at Posted at 2018-11-19

はじめに

Thymeleafで、バカの一つ覚えみたいにただただth:fieldを多用していたのですが、最近その理解レベルから脱却できたような気がするので、その内容についてまとめたいと思います。

概要としては、Spring Frameworkで値の受け渡しを担っているFormクラスにフォーカスした話になります。最終的にはFormクラスを一切使わないで値の受け渡しを行います。

狙いは以下の通りです。

  • Spring Frameworkの値の受け渡しの仕組みを知る。
  • Spring Frameworkの実装をより柔軟に考えられるようになる。

理解の仕方の話なので、あんまり実用的ではないです。

前提

Spring Frameworkで作成されたWebプロジェクトが必要です。
テンプレートエンジンはThymeleafを前提とした話になります。

一般的な実装

まずはFormを利用した一般的な実装を考えてみましょう。ここからコードをいじって、最終的にFormクラスを消してしまいます。

BookForm.java

public class BookForm {

    private String title;
    private int price;
    private String summary;
    //  Getter, Setterは省略
}

SampleController.java

@Controller
public class SampleController {

    //  入力画面
    @GetMapping("/input")
    public String input(@ModelAttribute("bookForm") BookForm bookForm) {
        return "input";
    }

    //  書籍情報画面
    @PostMapping("/bookinfo")
    public String toBookInfo(@ModelAttribute("bookForm") BookForm bookForm) {
        return "bookinfo";
    }
}

input.html

    <!-- bodyタブ内部のみ -->
    <h3>Formクラスを利用した実装</h3>
    <form th:action="@{/bookinfo}" method="post" th:object="${bookForm}">
    <label>タイトル</label><br>
    <input type="text" th:field="*{title}"><br>
    <label>値段</label><br>
    <input type="text" th:field="*{price}"><br>
    <label>概要</label><br>
    <input type="text" th:field="*{summary}"><br>
    <button>送信</button>
    </form>

bookinfo.html

    <!-- bodyタブ内部のみ -->
    <h3>Formクラスを利用した実装</h3>
    <div th:object="${bookForm}">
    <label>タイトル</label><br>
    <span th:text="*{title}"></span><br>
    <label>値段</label><br>
    <span th:text="*{price}"></span><br>
    <label>メモ</label><br>
    <span th:text="*{summary}"></span><br>
    </div>

入力した値が出力されるという、ものすごく簡単な実装になります。

スクリーンショット 2018-11-06 22.23.08.png

スクリーンショット 2018-11-06 22.28.54.png

input.htmlのth:fieldとth:objectを消す

まず、input.htmlからth:fieldth:objectを消すことを考えてみます。
ここで、th:fieldおよびth:objectの正体を知るために、Chromeのデベロッパーツールを使って、Thymeleafで記述したinput.htmlがどうなっているのか確認してみましょう。

スクリーンショット 2018-11-06 22.49.40.png

formタグからth:objectが消えて、th:fieldで記述していたタイトルを入力するテキストボックスは、<input type="text" id="title" name="title" value>となっていることが分かると思います。
端的に言えば、デベロッパーツールで表示された通りに書けばth:fieldとth:objectを消すことが可能です。
必要な記述だけに絞って書くと以下のようになります。

input.html

    <!-- bodyタブ内部のみ -->
    <h3>th:objectを消した実装</h3>
    <form th:action="@{/bookinfo}" method="post">
    <label>タイトル</label><br>
    <input type="text" name="title"><br>
    <label>値段</label><br>
    <input type="text" name="price"><br>
    <label>概要</label><br>
    <input type="text" name="summary"><br>
    <button>送信</button>
    </form>

このhtmlの記述でも、一般的な実装で示した「入力した値が出力される」処理を実現することができます。

th:objectの記述がなくなったということは、SampleController.javaのinputメソッドのFormクラスも消すことができます。

    //  入力画面
    @GetMapping("/input")
    public String input() {
        return "input";
    }

一番大事なことは、
inputタグのneme属性にFormクラスのフィールド名を指定すると、入力した値がそのフィールドに格納される
ということです。

この記事の言いたいことのほぼ全ては、この事実にあります。
(上記はth:fieldの振る舞いとして、実は厳密に正しい説明にはなっていません。詳しくはth:field と th:object によるフォームバインディング機能(inputタグ・基本入力系編)を参照して頂けたら幸いです)

Formクラスを入れ替える

上記の仕組みを念頭に置くと、こんなことができます。
例えば、BookFormとは異なる以下のようなMovieFormというFormクラスがあったとします。

MovieForm.java

public class MovieForm {

    private String title;
    private String summary;
    //  Getter, Setterは省略
}

(不自然な例で申し訳ないが、)このMovieFormクラスを使って映画化した書籍情報を、映画の情報としても扱いたいとします。bookinfo.htmlに、「映画情報としても送信」ボタンを用意して、あくまでBookFormを送信するように書き換えます。

bookinfo.html(大幅に変更)

    <!-- bodyタブ内部のみ -->
    <h3>BookFormからMoiveFormに値を入れ替える</h3>
    <form th:action="@{/movieinfo}" method="post" th:object="${bookForm}">
    <label>タイトル</label><br>
    <input type="text" readonly th:field="*{title}"><br>
    <label>値段</label><br>
    <input type="text" readonly th:field="*{price}"><br>
    <label>概要</label><br>
    <input type="text" readonly name="summary" th:value="*{summary}"><br>
    <button>映画情報としても送信</button>
    </form>

SampleController.java(追記)

    //  映画情報出力画面(追記分)
    @PostMapping("/movieinfo")
    public String toMovieinfo(@ModelAttribute("movieForm") MovieForm movieForm) {
        return "movieinfo";
    }

movieinfo.html

    <!-- bodyタブ内部のみ -->
    <h3>BookFormからMoiveFormに値を入れ替える</h3>
    <div th:object="${movieForm}">
    <label>タイトル</label><br>
    <span th:text="*{title}"></span><br>
    <label>概要</label><br>
    <span th:text="*{summary}"></span><br>
    </div>

処理結果は以下のようになリます。
スクリーンショット 2018-11-11 19.18.16.png
スクリーンショット 2018-11-11 19.18.31.png

このようにBookFormクラスとして送信した値が、MovieFormに格納され、movieinfo.htmlで出力されます。ちょっとひねくれた実装になっているので、不思議な心持ちになる人もいるかもしれません。

ポイントは、BookForm、MovieFormにtitlesummaryといった同じ名前のフィールドが存在しているところにあります。
bookinfo.htmlでは二種類書きましたが、今回の処理結果においては

<input type="text" readonly th:field="*{title}">
<input type="text" readonly name="title" value="あいうえお">

<input type="text" readonly name="summary" th:value="*{summary}">
<input type="text" readonly name="summary" value="テスト">

と同じ意味です。つまりname属性の値がFormのフィールド名と同じとなっているので、辻褄があっている訳です。

buttonに値を指定して、Formクラスで受け取る

あれ、th:field使わなくても実装できるのでは?
とそろそろ思えてきたのではないのでしょうか?

さてさて、今度は、buttonに指定された値を受け取ることを考えてみます。
MovieFormに以下のようなcategoryフィールドを追加して、bookinfo.htmlにカテゴリーごとのボタンを増やします。押したボタンに応じてmovieinfo.htmlにテキストが表示されます。

MovieForm.java

public class MovieForm {

    private String title;
    private String summary;
    // categoryフィールドを追加
    private String category;
    //  Getter, Setterは省略
}

bookinfo.html(categoryに値を指定したbuttonを複数追加)

    <!-- bodyタブ内部のみ -->
    <h3>buttonに値を指定して、Formクラスで受け取る</h3>
    <form th:action="@{/movieinfo}" method="post" th:object="${bookForm}">
    <label>タイトル</label><br>
    <input type="text" readonly th:field="*{title}"><br>
    <label>値段</label><br>
    <input type="text" readonly th:field="*{price}"><br>
    <label>概要</label><br>
    <input type="text" readonly name="summary" th:value="*{summary}"><br>
    <button name="category" value="アクション">アクション映画として送信</button>
    <button name="category" value="ファンタジー">ファンタジー映画として送信</button>
    <button name="category" value="SF">SF映画として送信</button>
    </form>

movieinfo.html

    <!-- bodyタブ内部のみ -->
    <h3>buttonに値を指定して、Formクラスで受け取る</h3>
    <div th:object="${movieForm}">
    <label>タイトル</label><br>
    <span th:text="*{title}"></span><br>
    <label>概要</label><br>
    <span th:text="*{summary}"></span><br>
    カテゴリーは<span th:text="*{category}"></span>です。
    </div>

結果は以下の通り

th:fieldばかり使っていると、入力できない項目のvalue値を変更することは難しいですが、nameの値とvalueの値が紐づいていることが分かると、このようにボタンのvalue値によって処理を変更することも可能になります。

Formクラスを消す

最終段階として、Formクラスを消してしまいます。ここで活躍するのはアノテーションの@RequestParamです。

入力した値が出力される実装は以下のようになります。

input.html
input.htmlのth:fieldとth:objectを消すを参照

SampleController.java

@Controller
public class SampleController {

    //  入力画面
    @GetMapping("/input")
    public String input() {
        return "input";
    }

    //  書籍情報出力画面
    @PostMapping("/bookinfo")
    public String toBookInfo(@RequestParam Map<String, String> bookmap, Model model) {
        model.addAttribute("bookmap", bookmap);
        return "bookinfo";
    }
}

bookinfo.html

    <!-- bodyタブ内部のみ -->
    <h3>Formクラスを消す</h3>
    <label>タイトル</label><br>
    <span th:text="${bookmap.title}"></span><br>
    <label>値段</label><br>
    <span th:text="${bookmap.price}"></span><br>
    <label>メモ</label><br>
    <span th:text="${bookmap.summary}"></span><br>

これでFormクラスを一切使わずに一般的な実装と同じ結果を得ることができます。
@RequestParamでinput.htmlで入力した値をMapで受け取ると、bookinfoの中では{title=あいうえお, price=1000, summary=テスト}の形で値が格納されます。この記事で何度も言っていたnameの値とvalueの値が紐づいて送られていることがよく分かるのではないでしょうか?

また、この実装のメリットは入力する値を可変にできることだったりします。

きっかけと補足

そもそも、「th:fieldを消したい」っと思ったのは、可変な入力画面を作成する必要に迫られたからでした。

色々やった結論としては、th:fieldは使えるんだったら積極的に使った方がいいし、@RequestParamまで使ってFormクラスを消すべきはないと思っています。

@RequestParamを使って入力値をMapで受け取った実装だと、
戻るボタンがあったときの値の処理
エラーハンドリング(Spring Frameworkのアノテーションが当然使えないっ)
などなど大いに困ります。

今回の例についても、buttonに値を指定して受け取るくらいはしていいかもしれませんがFormクラスを入れ替えるをやると可読性は格段に落ちたものになると思います。

まとめ

th:fieldはHTMLではnameとvalue(とid)属性を意味している
value属性の値はname属性に指定した値に紐づいて送られ、Controller側ではそれを受け取っている
以上です!

参考

Spring MVC populate @RequestParam Map
Spring MVC コントローラの引数

コメントにも記載ありますが、th:fieldの振る舞いについては、こちらの記事がより詳しく説明してあります。
th:field と th:object によるフォームバインディング機能(inputタグ・基本入力系編)

100
83
4

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
100
83