はじめに
本記事については以下のメイン教材を進めていく中で自分で追加検証したことや、忘れそうだなーってことを備忘録として残しております。
Spring Boot 入門:TODO アプリを作って学ぶ Web アプリケーション開発の基礎
また、僕なりの意見なども記録として記載してたりするのでノイズになる可能性が十分にあります。そういった意見など求めてない、でもSpring Boot入門したいって方はシンプルに上記動画教材を購入されることを強くオススメします。めちゃくちゃ説明が分かりやすいです。
Controller
@ModelAttribute
@ModelAttribute はどこで使用するかで動きが変わった記憶があるので随時判明次第更新予定
メソッドの引数部分で使用する場合
@GetMapping("test")
public String showTest(@ModelAttribute SampleForm form) {
// model.addAttribute("sampleForm", form); // ←@ModelAttributeにより、この処理と同じことをしてくれるので記述不要になる。
return "/test";
}
modelに登録する名前は自動的にパスカルケースからキャメルケースで登録されるが任意の名前に変更したい場合は以下(sampleFormからtestFormに変えた場合)
@GetMapping("test")
public String showTest(@ModelAttribute("testForm") SampleForm form) {
// model.addAttribute("testForm", form); // ←@ModelAttributeにより、この処理と同じことをしてくれるので記述不要になる。
return "/test";
}
また、@ModelAttributeを使用することで上記の場合SampleFormがnullの場合を考慮した初期化まで自動で実施してくれるし、Modelそのものの追加も自動で行われる。つまりこれらの働きにより以下のコメントアウト部分全てが不要となる。
@GetMapping("test")
public String showTest(@ModelAttribute SampleForm form,
// Model model ①modelの追加
) {
// ②SampleFormがnullだった場合の初期化処理
// if (form == null) {
// form = new SampleForm();
// }
// model.addAttribute("sampleForm", form); // ③modelへの追加
return "/test";
}
とまぁかなり便利なアノテーションではあるが、ここで注意点が1つ!!!
引数でsampleFormからtestFormに登録名をカスタムした場合に関係してくる話となるが@ModelAttributeの引数でカスタム名指定はやめたほうが良いと思う。
理由は画面から処理が飛んできた場合は正常に機能するが別メソッドからフォワードさせた場合はパスカルケースからキャメルケース変換されて登録されるため、引数で指定したカスタム名はシカトされる。なので使う場面など考慮することが増えるので素直にSpringのデフォルトの動きに従うほうが良いと個人的には思ってる。以下がその正常に機能しない例で③-1,③-2がこの話の観点。
@GetMapping("test")
public String showTest(@ModelAttribute("testForm") SampleForm form,
// Model model ①modelの追加は問題なし
) {
// ②SampleFormがnullだった場合の初期化処理も問題なし
// if (form == null) {
// form = new SampleForm();
// }
// model.addAttribute("testForm", form); // ③-1 modelへの追加 画面からGETで来た場合はこっち
// model.addAttribute("sampleForm", form); // ③-2 modelへの追加 storeメソッドのbindingResult.hasErrors()がtrueで呼び出された場合は引数のカスタム名ではなくキャメルケース変換された名前で登録されるため、test.html側でエラーが発生する。
return "/test";
}
@PostMapping
public String store(@Validated SampleForm form, BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
return form(form);
}
// 以下の処理は今回の話とは無関係なのでスルーしてOK
service.store(form.toEntity());
return "redirect:/test";
}
@PathVariable
基本形
@GetMapping("/{id1}")
public String detail(@PathVariable("id1") long id2, Model model) {
model.addAttribute("id3", id2);
return "detail"; // detailではid3の名前でid2を使用できる。また、id2の元はid1
}
数値2桁のみを受け付ける場合
@GetMapping("/{id:[0-9]{2}}")
public String detail(@PathVariable("id") long id, Model model) {
model.addAttribute("id", id);
return "detail";
}
数値のみを受け付ける場合
@GetMapping("/{id:[0-9].*")
public String detail(@PathVariable("id") long id, Model model) {
model.addAttribute("id", id);
return "detail";
}
このように数値のみに限らず@xxMappingアノテーションでは正規表現を使用することが可能。
そして@PathValiableを使用することでJava側で利用できるように受け取ることが出来る。
注意点は上記の記述で数値以外の文字列が飛んできたらlong型に変換できないためエラーが発生する。
URLで使用した文字列とメソッドの引数名が同じ場合
@PathVarivaleの引数は省略可能
@GetMapping("/{taskId:[0-9].*}")
public String detail(@PathVariable long taskId, Model model) {
model.addAttribute("taskId", taskId);
return "tasks/detail";
}
名前が違う場合URLの文字列と@PathVariableの引数と名前を一致させればOK
【追記】
@ModelAttributeと同様、@PathVariableで受け取った名前でmodelに自動で登録されているっぽい。サンプルコードは以下
@GetMapping("/{id:[0-9].*}")
public String detail(@PathVariable("id") long taskId, Model model) {
var taskEntity = taskService.findById(taskId)
.orElseThrow(() -> new IllegalArgumentException("Task not found: id = " + taskId));
model.addAttribute("task", TaskDTO.toDTO(taskEntity));
return "tasks/detail";
}
// 普通は上記のように複雑なことをわざわざしないほうが良いが対応関係をまとめると以下の通り。
// @GetMappingでidの名前で受け取っているため@PathVariableでもidとする必要あり。
// この時点でidという名前で自動でModelに追加されている。
// 一方で、変数名としてはtaskIdとしているのでメソッド内ではtaskIdとして利用可能
// 上記は対応関係説明のためで本来は以下がシンプルなので良いと思う。
@GetMapping("/{id:[0-9].*}")
public String detail(@PathVariable long id, Model model) {
var taskEntity = taskService.findById(id)
.orElseThrow(() -> new IllegalArgumentException("Task not found: id = " + id));
model.addAttribute("task", TaskDTO.toDTO(taskEntity));
return "tasks/detail";
}
PRGパターン
POSTリクエストを処理するハンドラーメソッドの戻り値はリダイレクトさせようね。っていう考え方。
これによって「多重送信」を防ぐことが出来る。
PRGパターンを採用していない例
/**
* 一覧表示
*
* @param model
* @return
*/
@GetMapping("/")
public String list(Model model) {
var list = service.findAll()
.stream()
.map(DTO::toDTO)
.toList();
model.addAttribute("list", list);
return "/list";
}
/**
* 登録処理
*
* @param form
* @param model
* @return
*/
@PostMapping("/")
public String store(StoreForm form, Model model) {
var newEntity = new Entity(form.xx(),
form.yy(),
);
service.store(newEntity);
return this.list(model); // 一覧表示のメソッド呼び出し
}
PRGパターンを採用した例
/**
* 一覧表示
*
* @param model
* @return
*/
@GetMapping("/")
public String list(Model model) {
var list = service.findAll()
.stream()
.map(DTO::toDTO)
.toList();
model.addAttribute("list", list);
return "/list";
}
/**
* 登録処理
*
* @param form
* @param model
* @return
*/
@PostMapping("/")
public String store(StoreForm form, Model model) {
var newEntity = new Entity(form.xx(),
form.yy(),
);
service.store(newEntity);
return "redirect:/"; // 一覧表示のメソッドへリダイレクト
}
PRGパターンを採用したことで変わること
- ブラウザのリロードによる多重送信を防げる。
- POSTの後にリダイレクトさせることで直前のリクエストがPOSTではなくGETに変わるため
- レスポンスステータスが200から302に変わる。
Service
@Transactional
@Transactional
public void store(Entity newEntity) {
repository.store(newEntity);
}
上記のような更新(CRUDのR以外の処理)処理を実施する場合はServiceのクラスに@Transactional
を記述することでトランザクション管理がよしなに行われる。なのでServiceを組み合わせるようなUseCaseクラスなどが存在するアーキテクチャの場合はServiceではなくUseCaseのメソッドにつけたほうが良いかも?
ACID特性でいう所のAtomicity(原子性)を担保することが可能。「All or Nothing」の精神ですな。