社員一覧の各行に「削除」ボタンを付けて、押した社員をデータベースから削除できるようにします。データを「変える」操作を作るのは、この章が初めてです。
この章で学ぶこと
- データを変える操作を POST で送る理由
-
@PostMappingと@PathVariableで、URL に含まれる社員IDを受け取る方法 - Repository の
deleteByIdで削除する方法と、トランザクション - 処理のあとに
redirect:で一覧画面へ戻す方法(PRG パターン)
9.1 この章で作るもの
第8章で作った社員一覧画面に「操作」列を追加し、各行に「削除」ボタンを置きます。ボタンを押すと、その行の社員を削除して、一覧画面を表示し直します。
この章で変更するファイルは、次の2つです。
| ファイル | 変更内容 |
|---|---|
EmployeeListController.java |
削除の処理を行う delete メソッドを追加する |
templates/employee/list.html |
「操作」列と「削除」ボタンを追加する |
9.2 Controller に削除の処理を追加する
- パッケージ・エクスプローラーで
src/main/javaのcom.example.employeeapp.controllerを開き、EmployeeListController.javaをダブルクリックして開きます。 - 次のように、色の付いた行を追加します。
package com.example.employeeapp.controller;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import com.example.employeeapp.entity.Employee;
import com.example.employeeapp.repository.EmployeeRepository;
@Controller
public class EmployeeListController {
@Autowired
private EmployeeRepository employeeRepository;
/** 社員一覧画面を表示
* 社員IDの順に社員を取り出して表示
* @param model 表示する社員情報
* @return 社員一覧画面 */
@GetMapping("/employees")
public String list(Model model) {
List<Employee> employees = employeeRepository.findAllByOrderByEmpIdAsc();
model.addAttribute("employees", employees);
return "employee/list";
}
/** 社員を削除して社員一覧画面に戻る
* @param empId 削除する社員の社員ID
* @return 社員一覧画面 */
@PostMapping("/employees/{empId}/delete")
public String delete(@PathVariable Integer empId) {
employeeRepository.deleteById(empId);
return "redirect:/employees";
}
}
-
Ctrl + S で保存します。
@PostMappingや@PathVariableは補完(Ctrl + Space)で入力すると、import 文も自動で追加されます(第4章「入力は補完で行う」)。
delete メソッドは、たった3行です。「URL から社員IDを受け取る」「その社員を削除する」「一覧画面へ移動させる」の3つを行っています。詳しくは 9.5 解説 で説明します。
9.3 一覧画面に削除ボタンを追加する
-
src/main/resources/templates/employee/list.htmlを開きます。 - 見出し行の最後に「操作」列を、社員の行の最後に削除ボタンを追加します。
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>社員一覧</title>
<link rel="stylesheet" th:href="@{/css/app.css}">
</head>
<body>
<h1>社員一覧</h1>
<table>
<tr>
<th>社員ID</th>
<th>氏名</th>
<th>性別</th>
<th>年齢</th>
<th>生年月日</th>
<th>部署</th>
<th>住所</th>
<th>操作</th>
</tr>
<tr th:each="employee : ${employees}">
<td th:text="${employee.empId}"></td>
<td th:text="${employee.name}"></td>
<td th:switch="${employee.gender}">
<span th:case="1">男</span>
<span th:case="2">女</span>
</td>
<td th:text="${employee.age}"></td>
<td th:text="${#dates.format(employee.birthDate, 'yyyy/MM/dd')}"></td>
<td th:text="${employee.department.deptName}"></td>
<td th:text="${employee.address}"></td>
<td>
<form class="inline" th:action="@{/employees/{empId}/delete(empId=${employee.empId})}" method="post">
<button type="submit">削除</button>
</form>
</td>
</tr>
</table>
</body>
</html>
- Ctrl + S で保存します。
【知識メモ】class="inline" について
<form> は、通常はそれだけで1行を使って表示されます。第8章で作った app.css には form.inline { display: inline; } という指定があり、class="inline" を付けたフォームは、文字と同じように横に並んで表示されます。第12章で「変更」ボタンを追加したとき、「変更」と「削除」を横に並べるために使います。
9.4 動作確認
- アプリを起動している場合は、コンソールの赤い四角(終了)ボタンで一度停止してから、プロジェクトを右クリック → 実行 → Spring Boot アプリケーション で起動し直します(Boot ダッシュボードの再起動ボタンでもかまいません)。停止している場合は、そのまま起動します。保存しただけでは、動いているアプリには反映されません。
- Chrome で
http://localhost:8080/employeesを開きます。各行の右端に「削除」ボタンが表示されていることを確認します(図9-1)。 - 林檎 星飛(社員ID 3)の行の「削除」ボタンを押します。
- 一覧画面が表示し直され、林檎 星飛の行がなくなっていれば成功です。

図9-2 林檎 星飛(社員ID 3)を削除したあとの社員一覧
SQL Developer で empapp に接続し、SELECT * FROM EMPLOYEE ORDER BY EMP_ID; を実行すると、データベースからも社員ID 3 の行が消えていることを確認できます。
【ご注意】削除したデータを元に戻したいとき
削除したデータを元に戻すボタンはありません。サンプルデータを最初の状態に戻したいときは、第3章「やり直したいとき」 の手順でテーブルを作り直してください。
なお、この教材の以降の画面は、社員ID 3(林檎 星飛)を削除した状態 で進みます。第11章で新しく登録する社員の社員IDは 4 になります(一度使われた番号は、削除しても再利用されません)。
9.5 解説
なぜ POST で送るのか
第1章で、「データを見るだけなら GET、データを変えるなら POST」と説明しました。削除はデータを変える操作なので、POST で送ります。
もし削除を GET で作ると、リンクをクリックしたり URL を開いたりするだけで削除が実行されてしまいます。ブラウザの履歴やブックマークから URL を開いただけで社員が消えてしまう、といった事故も起こり得ます。POST は、フォームのボタンを押したときに送られるので、このような「うっかり実行」を防げます。
HTML でボタンから POST を送るには、<form method="post"> の中に <button type="submit"> を置きます。
@PostMapping
@GetMapping が GET のリクエストを受け取るのに対して、@PostMapping は POST のリクエストを受け取る メソッドに付けます。
| アノテーション | 受け取るリクエスト | この章での例 |
|---|---|---|
@GetMapping("/employees") |
GET /employees | 一覧画面の表示 |
@PostMapping("/employees/{empId}/delete") |
POST /employees/3/delete など | 社員の削除 |
URL に社員IDを埋め込む
どの社員を削除するかをサーバーに伝えるため、この章では URL の中に社員IDを入れて 送っています。社員ID 3 の削除ボタンは、/employees/3/delete に POST します。
この URL を作っているのが、list.html の次の部分です。
th:action="@{/employees/{empId}/delete(empId=${employee.empId})}"
| 書き方 | 意味 |
|---|---|
@{ … } |
Thymeleaf で URL を作るときの書き方 |
{empId} |
あとから値を入れる場所(名前は empId) |
(empId=${employee.empId}) |
{empId} の場所に、その行の社員の社員IDを入れる |
実際に作られた HTML は、Chrome で確認できます。一覧画面の何もないところで右クリック → ページのソースを表示 を選ぶと、次のように行ごとに違う URL になっていることがわかります。
<form class="inline" action="/employees/1/delete" method="post">
<button type="submit">削除</button>
</form>
…
<form class="inline" action="/employees/3/delete" method="post">
<button type="submit">削除</button>
</form>
@PathVariable:URL から値を取り出す
Controller 側では、@PostMapping("/employees/{empId}/delete") の {empId} の部分に入っている値を、@PathVariable を付けた引数で受け取ります。
@PathVariable は、引数の名前(empId)から、URL のどの部分の値を受け取るかを決めます。そのため、@PathVariable("empId") のように名前を書く必要はありません。
@PostMapping("/employees/{empId}/delete")
public String delete(@PathVariable Integer empId) {
/employees/3/delete に POST されると、引数 empId に 3 が入ります。URL の中は文字ですが、引数の型を Integer にしておけば、Spring が数値に変換してくれます。
【ご注意】名前をそろえる
画面の {empId}、@PostMapping の {empId}、@PathVariable を付けた引数の名前 empId の3か所は、同じ名前にそろえます。どれか1つでも綴りが違うと、正しく受け取れません。
deleteById:削除する
deleteById は、第7章で紹介した JpaRepository に最初から用意されているメソッドです。引数に渡した ID の行を削除します。
employeeRepository.deleteById(empId);
実行されると、コンソールに Hibernate が発行した SQL が表示されます。削除の前に、対象の社員を検索する SELECT も実行されています。
Hibernate:
select
e1_0.emp_id,
…
from
employee e1_0
left join
department d1_0
on d1_0.dept_id=e1_0.dept_id
where
e1_0.emp_id=?
Hibernate:
delete
from
employee
where
emp_id=?
トランザクションと COMMIT
第3章では、INSERT のあとに COMMIT を実行しないと、登録した内容が確定しないことを学びました。では、deleteById のあとに COMMIT は必要でしょうか。
答えは「自分で書く必要はない」です。deleteById や、第11章で使う save などの Repository のメソッドは、Spring Data JPA の中で 自動的にトランザクションが開始され、処理が終わると COMMIT されます。そのため、この教材のように Repository のメソッドを1回呼ぶだけの処理では、トランザクションを意識しなくても正しく動きます。
【知識メモ】トランザクションとは
複数の処理を「すべて成功」か「すべて取り消し」のどちらかにまとめる仕組みです。業務システムで「複数のテーブルをまとめて更新する」といった処理を作るときは、@Transactional というアノテーションで範囲を指定します。この教材では使いませんが、名前だけ覚えておきましょう。
redirect: と PRG パターン
delete メソッドは、テンプレート名ではなく "redirect:/employees" を返しています。先頭に redirect: を付けると、Spring は画面を返す代わりに、ブラウザに「/employees を開き直してください」と伝えます。これを リダイレクト といいます。第1章で紹介したステータスコード 302 は、このときに使われます。
図9-3 削除からリダイレクトまでの流れ
削除のあと、一覧のテンプレート名("employee/list")を直接返してもよさそうに見えます。しかしそうすると、ブラウザは「POST /employees/3/delete の結果」として一覧画面を表示することになります。この状態で利用者がブラウザの 再読み込み(F5)を押すと、同じ POST がもう一度送信されてしまいます。
リダイレクトしておけば、ブラウザが最後に送ったリクエストは GET /employees になります。再読み込みしても一覧を表示し直すだけで、POST が再送されることはありません。
このように、POST で処理したら Redirect し、GET で画面を表示する作り方を、頭文字をとって PRG パターン といいます。データを変える処理のあとは、PRG パターンにするのが基本です。第11章の新規登録、第12章の変更でも使います。
9.6 よくあるエラー
| 症状 | 主な原因 | 対処 |
|---|---|---|
| 「削除」を押すと 405(Method Not Allowed)になる |
<form> に method="post" を書き忘れて、GET で送られている |
list.html の削除フォームに method="post" があるか確認する |
| 「削除」を押すと 404 になる |
th:action の URL の書き方が違う(/delete の付け忘れなど) |
「ページのソースを表示」で action="/employees/3/delete" のようになっているか確認する |
| 「削除」を押すと 400(Bad Request)になる |
th:action の {empId} と (empId=…) の名前が一致していない。{empId} が社員IDに置き換わらず、action="/employees/{empId}/delete?empid=3" のようになる |
{empId} と (empId=…) の綴りをそろえる。送られた URL は付録D の方法でも確かめられる |
| 「削除」を押すと 500 になり、コンソールに「Required URI template variable 'id'」などと出る |
@PostMapping の {empId} と、@PathVariable を付けた引数の名前が一致していない |
3か所の名前を empId にそろえる |
| 削除はされるが、Whitelabel エラー画面(500)になる |
"redirect:/employees" の綴りが違う(redirect の打ち間違い、: の抜けなど) |
戻り値を "redirect:/employees" に直す |
【知識メモ】外部キーのエラーは起きない?
第3章で、社員テーブルから部署テーブルへの外部キー制約を作りました。部署を削除しようとすると、その部署に所属する社員がいる場合にエラー(ORA-02292)になります。しかし社員は他のテーブルから参照されていないので、社員の削除ではこのエラーは起きません。
9.7 まとめと確認問題
まとめ
- データを変える操作は、
<form method="post">を使って POST で送る -
@PostMappingを付けたメソッドで POST のリクエストを受け取る -
@{/employees/{empId}/delete(empId=${employee.empId})}のように書くと、URL に社員IDを埋め込める。Controller では@PathVariableで受け取る -
deleteByIdで削除できる。Repository のメソッドは自動的にトランザクションの中で実行され、COMMIT される - 処理のあとは
"redirect:/employees"でリダイレクトする(PRG パターン)。再読み込みで POST が再送されるのを防げる
確認問題
問1 削除を GET ではなく POST で作るのはなぜですか。
解答を見る
GET で作ると、リンクのクリックや URL を開くだけで削除が実行されてしまい、履歴やブックマークからの「うっかり実行」が起こり得るためです。データを変える操作は POST で送るのが基本です。
問2 社員ID 2 の行の削除ボタンは、どの URL に POST しますか。
解答を見る
/employees/2/delete です。@{/employees/{empId}/delete(empId=${employee.empId})} の {empId} の場所に、その行の社員ID 2 が入ります。
問3 @PathVariable Integer empId は何をしていますか。
解答を見る
@PostMapping("/employees/{empId}/delete") の {empId} の部分に入っている値を取り出し、Integer に変換して引数 empId に入れています。どの部分の値を取り出すかは、引数の名前 empId で決まります。
問4 deleteById のあとに COMMIT を実行するコードを書いていないのに、削除が確定するのはなぜですか。
解答を見る
Repository のメソッドは、Spring Data JPA の中で自動的にトランザクションが開始され、処理が終わると COMMIT されるためです。
問5 削除のあとに "employee/list" を返すのではなく、"redirect:/employees" を返すのはなぜですか。
解答を見る
テンプレートを直接返すと、ブラウザの再読み込みで削除の POST がもう一度送信されてしまうためです。リダイレクトすると最後のリクエストが GET /employees になり、再読み込みしても一覧を表示し直すだけになります(PRG パターン)。

