📝

【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)と結びつけられます。その結果、フォーム内に記述される各入力項目は、そのオブジェクトのプロパティ(nameidなど)に対して**自動的にデータの送受信(バインド)**が行われるようになります。
    具体的には、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