📝
【Spring Boot】編集(更新)機能をバリデーション付きで実装する
🎯 今回やること
前回はカテゴリの「作成(POST)」処理をバリデーション付きで実装しました。
今回はその続きとして、既存カテゴリの編集(更新)機能を作成していきます。
画面で「編集」ボタンをクリック → 入力フォームに遷移 → 更新して保存、という流れです。
🖥 編集リンクを一覧に追加
まずはカテゴリ一覧に「編集」ボタンを追加します。

list.html
<!-- list.htmlの一部 -->
<!-- category.id がnullになるとリンク生成エラーになるので注意 -->
<!-- th:href の中で + を使うことでパス動的生成 -->
<!-- ${}(EL式)を使って category.id を埋め込み、URLを動的に生成 -->
<a th:href="@{'/categories/edit/' + ${category.id}}" class="btn btn-primary btn-sm me-1">編集</a>
クリックすると /category/edit/{id} に遷移します。
このURLに対応するコントローラで、該当カテゴリ情報を取得し、編集フォームに表示させます。
🛠 編集画面を作る(GETリクエスト)
次に、該当カテゴリを取得し、フォームに初期値を表示させます。
@GetMapping("/edit/{id}")
public String editForm(@PathVariable Long id, Model model) {
Optional<Category> categoryOpt = categoryService.findById(id);
if (categoryOpt.isEmpty()) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "指定されたカテゴリが見つかりませんでした");
}
model.addAttribute("category", categoryOpt.get());
return "category/form";
}
-
Optional<>「値があるかもしれないし、ないかもしれない」ことを安全に表現するためのラッパークラスです。null を直接扱う代わりに、isPresent() や orElse() などで安全に処理できます。主にメソッドの戻り値で使われ、NullPointerException の回避に役立ちます。
ここまでの流れを図解すると、↓のようになります。
(※画像が見にくい場合は、画像をクリックした後にCtrlキーを2回押し、拡大モードに切り替えてください。)

📝 編集フォーム(edit.html)
以下のようなフォームを作成します。

<form th:action="@{${category.id == null} ? '/categories' : '/categories/update/' + ${category.id}}"
th:object="${category}" method="post">
<div class="mb-3">
<label for="name" class="form-label">カテゴリ名:</label>
<input type="text" th:field="*{name}" id="name" class="form-control" />
<!-- バリデーションエラーメッセージ -->
<div th:if="${#fields.hasErrors('name')}" class="text-danger mt-1">
<p th:errors="*{name}">カテゴリ名エラー</p>
</div>
</div>
<button type="submit" class="btn btn-primary">保存</button>
<a th:href="@{/categories}" class="btn btn-secondary ms-2">← 戻る</a>
</form>
-
th:actionにより、category.id == nullかどうかでPOST先を切り替えています。
この切り替えにより、作成と更新をフォーム1つで共通化しています。 -
th:object="${category}"は、Thymeleafにおけるフォームのバインド対象オブジェクトを指定する属性です。
この属性を使うことで、フォーム全体が特定のJavaオブジェクト(この例ではcategory)と結びつけられます。その結果、フォーム内に記述される各入力項目は、そのオブジェクトのプロパティ(nameやidなど)に対して**自動的にデータの送受信(バインド)**が行われるようになります。
具体的には、th:object="${category}" をフォームの form タグに指定すると、
フォーム内で *{name} や *{id} のように記述することで、
あたかも category.name や category.id を直接操作しているかのように扱えるようになります。 - バリデーションメッセージ表示部分
#fields.hasErrors('name')で、name フィールドにバリデーションエラーがあるかをチェックします。
th:if="..."で、エラーがある場合だけこの <div> 全体を表示します。
th:errors="*{name}"で、実際のエラーメッセージ(例:「空欄は許可されていません」)を表示します。
ここまでの流れを図解すると、↓のようになります。
(※画像が見にくい場合は、画像をクリックした後にCtrlキーを2回押し、拡大モードに切り替えてください。)
編集画面初期表示時

バリデーションエラー発生時

🚀 更新処理(POSTリクエスト)
フォームからのPOSTを受け取り、バリデーション&更新を行います。
@PostMapping("/update/{id}")
public String update(@PathVariable Long id, @ModelAttribute @Valid Category category, BindingResult result) {
if (result.hasErrors()) {
return "category/form";
}
// パス変数idをエンティティにセット(セキュリティやバインド対策)
category.setId(id);
categoryService.save(category);
return "redirect:/categories";
}
🧼 サービス層での更新処理
// 新規作成 or 更新(idの有無で判断)
public Category save(Category category) {
// JPAがidの有無でINSERT/UPDATEを判定する
// idがnullならINSERT、それ以外はUPDATEされる(JPAの仕様)
return categpryRepository.save(category);
}
更新時の流れを図解すると、↓のようになります。
(※画像が見にくい場合は、画像をクリックした後にCtrlキーを2回押し、拡大モードに切り替えてください。)

✅ 動作確認
-
編集リンクからフォームに遷移できるか?

-
バリデーションが効いているか?


-
正しく更新され、一覧に反映されるか?

📝 まとめ
今回はカテゴリの編集機能を実装しました。
次回は削除機能や、ログ側での「カテゴリでの絞り込み」機能にも取り組んでいく予定です!
Discussion