Spring | @ModelAttributeを攻略したい | Java

Spring

@ModelAttributeはメソッドのアノテーションとして使う場合と、引数のアノテーションとして使う場合があります。両方を組み合わせて利用する場合もあります。

一体なんなんだ?と頭が混乱しましたので整理しました。

@ModelAttributeの正体はモデル(Model)からオブジェクトを取得するという処理です。
これを徐々に紐解いていきます。

というわけで、今回は以下のオブジェクトをコントローラーの実行前に取得してみます

public class Product {
    public final int id;
    public final String name;
    public Product(int id,String name){
        this.id = id;
        this.name = name;
    }
    @Override
    public String toString(){
        return "id = " + id + " name = " + name;
    }
}

何もせずに取得する

実は特段何も記載しなくても取得することができます。
取得するにはクエリパラメータとクラスのプロパティ名を合わせて送信します。

ここでのポイントは以下です

  • 引数で事前に生成したクラスのオブジェクトを受け取る
  • クエリパラメータにクラスのプロパティ名と合った値を設定して送信する
  • @ModelAttributeを利用していない
@GetMapping("/att")
public String sample(Product product){
    Assert.notNull(product,"product must not be null");
    System.out.println(product);
    return "sample";
}

リクエスト例:http://localhost:8080/att?id=1&name=ドリル

ModelAttributeアノテーションを付けなくてもコントローラーのオブジェクトが生成されていることがわかりました。

Modelに付与されるModelAttribute

実はこの時ModelオブジェクトにModelAttributeオブジェクトとして登録されている。Modelオブジェクトを見るとProductクラスのオブジェクトが格納されていることがわかる

この時クラス名を小文字にしたものがオブジェクトの名前になっているが、@ModelAttributeを利用してこれを変更できる

以下はproductという名前からmyProductという名前に変更する例です

@GetMapping("/att")
public String sample(@ModelAttribute("myProduct") Product product, Model model){
    Assert.notNull(product,"product must not be null");
    System.out.println(product);
    return "sample";
}

ちなみに以下のようなクエリパラメータを利用しない場合のURIテンプレートでも利用できる

@GetMapping("/att/productId/{id}/productName/{name}")
public String sample2(Product product, Model model){
    Assert.notNull(product,"product must not be null");
    System.out.println(product);
    return "sample";
}

以下のURLでアクセス:http://localhost:8080/att/productId/2/productName/ドラゴンボール

しっかりとProductオブジェクトが作成されています

メソッドに付けて事前処理を行う

じゃあ結局ModelAttributeアノテーションって何なんだ?という話の前に、メソッドに付けて実行させるパターンを見てみます。

ポイントは以下です

  • メソッドの前に@ModelAttribute
  • コントローラーのGetメソッドでは特に何もしてない
    • Modelだけ受け取っている
@ModelAttribute("myAtt")
public Model preExecute(Model model){
    Product product = new Product(
            99,
            "テスト製品"
    );
    model.addAttribute("myProduct",product);
    return model;
}

@GetMapping("/testpre")
public String testpre(Model model){
    System.out.println(model);
    return "sample";
}

modelにはmyProductというProductのオブジェクトが格納されており、アノテーションのnameで指定したmyAttというオブジェクトも格納されていることがわかります。

Productのオブジェクトは事前に用意したテスト製品が設定されています。

メソッドの前に@ModelAttributeを付けると、事前に処理を行いそれをModelに設定しておくことができます。

では、このままですよ?このまま引数だけ変更してProductを利用してみます。

  • 引数のModelを除去して直接Productを受け取ってみます
@GetMapping("/testpre")
public String testpre(Product product){
    System.out.println(product);
    return "sample";
}

しかし実行するとエラーになる

Resolved [org.springframework.web.method.annotation.ModelAttributeMethodProcessor$1: org.springframework.validation.BeanPropertyBindingResult: 1 errors<EOL>Field error in object 'product' on field 'id': rejected value [null]; codes [typeMismatch.product.id,typeMismatch.id,typeMismatch.int,typeMismatch]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [product.id,id]; arguments []; default message [id]]; default message [Failed to convert value of type 'null' to required type 'int'; nested exception is org.springframework.core.convert.ConversionFailedException: Failed to convert from type [null] to type [int] for value 'null'; nested exception is java.lang.IllegalArgumentException: A null value cannot be assigned to a primitive type]]

ここで@ModelAttributeアノテーションを利用してみましょう。そして名前を事前に設定したkeyであるmyProductを指定します

@GetMapping("/testpre")
public String testpre(@ModelAttribute("myProduct") Product product){
    System.out.println(product);
    return "sample";
}

なんと取得できていました。これで@ModelAttributeの動きがなんとなく見えてきたのではないでしょうか?

では次keyを指定せずに実行してみます

@GetMapping("/testpre")
public String testpre(@ModelAttribute Product product){
    System.out.println(product);
    return "sample";
}

これはさっきと同じエラーになります。事前処理のkey名を指定してあげないと取得できないようですね。

では事前処理を修正してみます。クラス名を小文字にしたデフォルト名にします

@ModelAttribute("myAtt")
public Model preExecute(Model model){
    Product product = new Product(
            99,
            "テスト製品"
    );
    model.addAttribute("product",product);
    return model;
}

今度は取得できました。

これでわかったように、ModelAttributeがやっていることはモデルからオブジェクトの取り出しです。

最終確認

  • ModelAttributeはモデルからオブジェクトを取り出している
  • key名が指定されているオブジェクトの場合は、そのkeyを指定する必要がある
  • デフォルトではクラス名を小文字にしたものである

最初の「何もせずに取得する」で確認したように、そもそもURLのパターンやパラメータによって引数に指定したクラスは生成されてモデルに格納されます。

なので引数にModelAttributeを付ける動機はモデルからの取得です。もしも、そもそもモデルに事前設定してない場合はアノテーションをつける必要は無さそうです。
→パラメータで指定した値でオブジェクトを生成するためにModelAttributeというのは間違い

ただしViewに返却するモデルに対して指定の名称を付けたい場合は@ModelAttribute(“name”)としてkeyを指定する必要があります

既にモデルにproductという名前でオブジェクトが登録されているときに、ModelAttributeでmyProductというオブジェクトを取得しようとすると、myProductは取得できないので新しく生成されてモデルに格納されることになります

@GetMapping("/att")
public String sample(@ModelAttribute("myProduct") Product product, Model model){
    Assert.notNull(product,"product must not be null");
    System.out.println(product);
    return "sample";
}
id1でnameを宝箱というパラメータを送信した結果

エラーになるについての捕捉

ちなみに何度かエラーになることを確認していますが、デフォルトコンストラクタを利用すればエラーになることはありませんが、何の値も設定されていないオブジェクトが生成されます。

事前に@ModelAttributeによってモデルに何も設定しておらず、以下のメソッドでProductを取得しようとした時に、取得できない場合は生成されますがデフォルトコンストラクタがあればそれが実行されます

@GetMapping("/testpre")
public String testpre(@ModelAttribute Product product){
    System.out.println(product);
    return "sample";
}

デフォルトコンストラクタを定義

public class Product {
    public final int id;
    public final String name;
    public Product(){
        this.id = 1;
        this.name = "デフォルト";
    }
    public Product(int id,String name) {
        this.id = id;
        this.name = name;
    }
    @Override
    public String toString(){
        return "id = " + id + " name = " + name;
    }
}

Productにはデフォルトコンストラクタで生成されたオブジェクトが設定されています。
この時は@ModelAttributeは不要です

もちろん事前メソッドがあり既にモデルにオブジェクトが設定されている場合は、そのオブジェクトを取得します

注意点として@ModelAttributeが引数にない場合でも、事前に設定している場合はそれを取得します。

登録されてないkey名を指定すれば、取得できずに新しく生成することになります

動き的にはメソッドの引数にはデフォルトで名前の指定がない@ModelAttributeが付与されているように見える。。

ただこれでModelAttributeが出てきてもビビることはなくなったと思います。

コメント

タイトルとURLをコピーしました