社員一覧の各行に「変更」ボタンを付け、その社員のデータが入った変更画面 → 確認画面 → 完了画面の流れを作ります。仕組みは第10・11章の新規登録とほとんど同じです。違うところに注目しながら進めましょう。
この章で学ぶこと
- データベースのデータをフォームに詰め替えて、変更画面に表示する方法
- URL に社員ID を入れて、どの社員を変更するかを伝える方法(
@PathVariable) -
saveメソッドでデータを更新する方法
12.1 この章で作るもの
社員一覧の各行に「変更」ボタンを付けます。ボタンを押すと、その社員の今のデータが入った変更画面が開きます。内容を直して「確認」を押すと確認画面へ、確認画面で「変更」を押すと完了画面へ進みます。
新規登録との違い
画面の流れは新規登録と同じですが、次の点が違います。
| 項目 | 新規登録(第10・11章) | 変更(この章) |
|---|---|---|
| 入力画面の最初の状態 | 空欄 | その社員の 今のデータが入っている |
| 社員ID | まだ決まっていない(登録時に自動で振られる) | 決まっている(表示するだけで変更はできない) |
| URL |
/employees/register など |
/employees/1/edit のように 社員ID が入る
|
| セッションに保存する名前 | registerForm |
updateForm |
save で実行される SQL |
INSERT(登録) | UPDATE(更新) |
12.2 一覧画面に「変更」ボタンを付ける
src/main/resources/templates/employee/list.html を開き、「削除」ボタンのフォームの前に「変更」ボタンのリンクを追加します。
<td>
<a class="button" th:href="@{/employees/{empId}/edit(empId=${employee.empId})}">変更</a>
<form class="inline" th:action="@{/employees/{empId}/delete(empId=${employee.empId})}" method="post">
<button type="submit">削除</button>
</form>
</td>
変更画面を 表示するだけ なので、「新規登録」ボタン(第10章)と同じように、リンクで作っています(データを変えるのは、確認画面で「変更」ボタンを押したときです)。社員ID 1 の行では、リンク先が /employees/1/edit になります。
12.3 フォームと Controller を作る
UpdateForm を作る
変更画面の入力を受け取るフォームクラスを作ります。変更画面の項目は、新規登録の項目に 社員ID を足したものです。そこで、第10章の RegisterForm を 継承 して、社員ID だけを追加します。
-
com.example.employeeapp.formパッケージを右クリックし、新規 → クラス を選びます。名前にUpdateFormと入力して 完了 を押します。 - 次のコードを入力します。getter と setter は、ソース → getter および setter の生成 で作ります。
package com.example.employeeapp.form;
public class UpdateForm extends RegisterForm {
private static final long serialVersionUID = 1L;
private Integer empId;
public Integer getEmpId() {
return empId;
}
public void setEmpId(Integer empId) {
this.empId = empId;
}
}
氏名や住所などのフィールドと getter・setter は、RegisterForm から引き継ぎます。RegisterForm は Serializable を実装しているので、UpdateForm もセッションに保存できます。
Controller を作る
com.example.employeeapp.controller パッケージを右クリックし、新規 → クラス を選びます。名前に EmployeeUpdateController と入力して 完了 を押し、次のように書きます。
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.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.SessionAttributes;
import org.springframework.web.bind.support.SessionStatus;
import com.example.employeeapp.entity.Department;
import com.example.employeeapp.entity.Employee;
import com.example.employeeapp.form.UpdateForm;
import com.example.employeeapp.repository.DepartmentRepository;
import com.example.employeeapp.repository.EmployeeRepository;
@Controller
@SessionAttributes("updateForm")
public class EmployeeUpdateController {
@Autowired
private EmployeeRepository employeeRepository;
@Autowired
private DepartmentRepository departmentRepository;
/** セッションに UpdateForm オブジェクトがないとき呼ばれる
* @return 空の UpdateForm オブジェクト */
@ModelAttribute
public UpdateForm createForm() {
return new UpdateForm();
}
/** 社員変更の入力画面を表示
* その社員の今のデータをフォームに入れて表示
* @param empId 変更する社員ID(取得元:URL)
* @param model 表示する社員情報と部署一覧
* @return 社員変更の入力画面 */
@GetMapping("/employees/{empId}/edit")
public String input(@PathVariable Integer empId, Model model) {
Employee employee = employeeRepository.findByEmpId(empId);
UpdateForm updateForm = new UpdateForm();
updateForm.setEmpId(employee.getEmpId());
updateForm.setName(employee.getName());
updateForm.setGender(employee.getGender());
updateForm.setAge(employee.getAge());
updateForm.setBirthDate(employee.getBirthDate());
updateForm.setDeptId(employee.getDepartment().getDeptId());
updateForm.setAddress(employee.getAddress());
model.addAttribute("updateForm", updateForm);
List<Department> departments = departmentRepository.findAllByOrderByDeptIdAsc();
model.addAttribute("departments", departments);
return "employee/update/input";
}
/** 社員変更の確認画面を表示
* @param updateForm 入力された社員情報
* @param model 表示する部署情報
* @return 社員変更の確認画面 */
@PostMapping("/employees/{empId}/edit/confirm")
public String confirm(@ModelAttribute UpdateForm updateForm, Model model) {
Department department = departmentRepository.findByDeptId(updateForm.getDeptId());
model.addAttribute("department", department);
return "employee/update/confirm";
}
/** 社員変更の確認画面から入力画面に戻る
* @param updateForm 入力された社員情報
* @param model 表示する部署一覧
* @return 社員変更の入力画面 */
@GetMapping("/employees/{empId}/edit/back")
public String back(@ModelAttribute UpdateForm updateForm, Model model) {
List<Department> departments = departmentRepository.findAllByOrderByDeptIdAsc();
model.addAttribute("departments", departments);
return "employee/update/input";
}
/** 社員変更の完了画面を表示
* @param updateForm 入力された社員情報
* @param sessionStatus セッション破棄時に利用
* @return 社員変更の完了画面 */
@PostMapping("/employees/{empId}/edit/complete")
public String complete(@ModelAttribute UpdateForm updateForm, SessionStatus sessionStatus) {
Employee employee = employeeRepository.findByEmpId(updateForm.getEmpId());
employee.setName(updateForm.getName());
employee.setGender(updateForm.getGender());
employee.setAge(updateForm.getAge());
employee.setBirthDate(updateForm.getBirthDate());
employee.setDepartment(departmentRepository.findByDeptId(updateForm.getDeptId()));
employee.setAddress(updateForm.getAddress());
employeeRepository.save(employee);
// セッションに保存していた入力内容を破棄する
sessionStatus.setComplete();
return "redirect:/employees/edit/done";
}
/** 社員変更の完了画面を表示
* @return 社員変更の完了画面 */
@GetMapping("/employees/edit/done")
public String done() {
return "employee/update/complete";
}
}
第11章の EmployeeRegisterController と見比べてみてください。createForm と、確認・戻る・完了のメソッド構成はほぼ同じです。新しく登場したのは、次のものです。
-
@PathVariable… URL に含まれる社員ID を受け取る(12.6 で解説)
【再確認】クラス名やアノテーションは補完で入力する
@PathVariable、Model などは、途中まで打って Ctrl + Space で補完すると、import 文も自動で追加されます(第4章「入力は補完で行う」)。Model は Model - org.springframework.ui を選んでください。手で打ち込んで赤い波線が出たときは、語の末尾で Ctrl + Space を押して選び直します。
12.4 テンプレートを作る
src/main/resources/templates/employee フォルダーを右クリックし、新規 → フォルダー で update フォルダーを作ります。その中に、次の3つのファイルを作ります(新規 → ファイル)。
| ファイル | 画面 |
|---|---|
update/input.html |
変更画面 |
update/confirm.html |
確認画面 |
update/complete.html |
完了画面 |
変更画面(input.html)
新規登録の入力画面(register/input.html)をもとにしています。違うのは、見出し、送信先の URL に社員ID を入れていること、社員ID を表示する行があること です。
<!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>
<form th:action="@{/employees/{empId}/edit/confirm(empId=${updateForm.empId})}" th:object="${updateForm}" method="post">
<table>
<tr>
<th>社員ID</th>
<td th:text="*{empId}"></td>
</tr>
<tr>
<th>氏名</th>
<td>
<input type="text" th:field="*{name}">
</td>
</tr>
<tr>
<th>性別</th>
<td>
<label><input type="radio" th:field="*{gender}" value="1"> 男</label>
<label><input type="radio" th:field="*{gender}" value="2"> 女</label>
</td>
</tr>
<tr>
<th>年齢</th>
<td>
<input type="text" th:field="*{age}">
</td>
</tr>
<tr>
<th>生年月日</th>
<td>
<input type="date" th:field="*{birthDate}">
</td>
</tr>
<tr>
<th>部署</th>
<td>
<select th:field="*{deptId}">
<option value="">選択してください</option>
<option th:each="department : ${departments}"
th:value="${department.deptId}"
th:text="${department.deptName}"></option>
</select>
</td>
</tr>
<tr>
<th>住所(都道府県)</th>
<td>
<input type="text" th:field="*{address}">
</td>
</tr>
</table>
<button type="submit">確認</button>
</form>
<p><a th:href="@{/employees}">一覧へ戻る</a></p>
</body>
</html>
確認画面(confirm.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>
<p>次の内容に変更します。よろしければ「変更」ボタンを押してください。</p>
<table th:object="${updateForm}">
<tr><th>社員ID</th><td th:text="*{empId}"></td></tr>
<tr><th>氏名</th><td th:text="*{name}"></td></tr>
<tr><th>性別</th><td th:switch="*{gender}"><span th:case="1">男</span><span th:case="2">女</span></td></tr>
<tr><th>年齢</th><td th:text="*{age}"></td></tr>
<tr><th>生年月日</th><td th:text="*{#dates.format(birthDate, 'yyyy/MM/dd')}"></td></tr>
<tr><th>部署</th><td th:text="${department?.deptName}"></td></tr>
<tr><th>住所(都道府県)</th><td th:text="*{address}"></td></tr>
</table>
<a class="button" th:href="@{/employees/{empId}/edit/back(empId=${updateForm.empId})}">戻る</a>
<form class="inline" th:action="@{/employees/{empId}/edit/complete(empId=${updateForm.empId})}" method="post">
<button type="submit">変更</button>
</form>
</body>
</html>
完了画面(complete.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>
<p>社員の変更が完了しました。</p>
<p><a th:href="@{/employees}">一覧へ戻る</a></p>
</body>
</html>
12.5 動作確認
- すべてのファイルを保存します(Ctrl + Shift + S で全部保存できます)。アプリを停止してから起動し直します。
- Chrome で
http://localhost:8080/employeesを開きます。各行に「変更」ボタンが表示されていることを確認します。
- 雷禅 海皇(社員ID 1)の行の「変更」ボタンを押します。雷禅 海皇の今のデータが入った変更画面が表示されます。
- 年齢を
27、住所を神奈川県に書き換えて、「確認」ボタンを押します。
- 「戻る」ボタンを押すと、書き換えた内容のまま変更画面に戻ることも確かめましょう。確かめたら、もう一度「確認」を押します。
- 確認画面で「変更」ボタンを押します。完了画面が表示されます。
- 「一覧へ戻る」を押します。雷禅 海皇の年齢が 27、住所が神奈川県に変わっていれば成功です。
Eclipse のコンソールを見ると、「変更」ボタンを押したときに、次のような UPDATE 文が実行されていることがわかります(表示は環境によって少し異なります)。
Hibernate:
update
employee
set
address=?,
age=?,
birth_date=?,
dept_id=?,
gender=?,
name=?
where
emp_id=?
12.6 解説:変更の流れ
変更画面を表示する(input メソッド)
変更画面では、最初からその社員のデータを入力欄に表示する必要があります。そこで、input メソッドでは次の順に処理しています。
-
employeeRepository.findByEmpId(empId)で、データベースからその社員の Entity を取り出す - 新しい
UpdateFormを作り、Entity の値を1つずつフォームに詰め替える - フォームを
updateFormという名前で Model に入れる(@SessionAttributesにより、セッションにも保存される)
詰め替えで1つだけ注意が必要なのが部署です。Entity の Employee は部署を Department オブジェクトとして持っていますが(第6章)、フォームの UpdateForm は部署ID(deptId)を持っています。そのため、employee.getDepartment().getDeptId() で部署ID を取り出してフォームに入れています。こうしておくと、プルダウンで今の部署が選ばれた状態で表示されます。
URL に社員ID を入れる
変更の画面では、どの社員を変更しているのかを URL で表します。
| 書き方 | 意味 |
|---|---|
@GetMapping("/employees/{empId}/edit") |
URL の {empId} の部分は、どんな値でも受け付ける。/employees/1/edit も /employees/2/edit もこのメソッドが処理する |
@PathVariable Integer empId |
URL の {empId} の部分の値を、引数 empId で受け取る。引数の名前 empId から、URL のどの部分かが決まる。/employees/1/edit なら 1 が入る |
@{/employees/{empId}/edit/confirm(empId=${updateForm.empId})} |
テンプレートで URL を組み立てる。{empId} の部分に、フォームの社員ID の値が入る |
@PathVariable と、テンプレートでの URL の組み立て方は、第9章の削除でも使いました。変更では、変更画面・確認画面・戻る・完了のすべての URL に社員ID を入れて、どの社員の変更かが URL で分かるようにしています。完了画面を表示する /employees/edit/done だけは、変更が終わって社員ID が不要なので入れていません。
社員ID は表示するだけ
変更画面の社員ID は、th:text="*{empId}" で 表示しているだけ で、入力欄にはしていません。社員ID は主キーなので、変えてしまうと別の社員のデータになってしまうからです。
データを更新する(complete メソッド)
確認画面で「変更」ボタンを押すと、complete メソッドが動きます。
-
findByEmpIdで、データベースから 今の 社員のデータを取り出し直す - 取り出した Entity の値を、フォームの値で上書きする
-
employeeRepository.save(employee)で保存する -
sessionStatus.setComplete()でセッションの入力内容を破棄し、完了画面にリダイレクトする
第7章で学んだとおり、save メソッドは 主キーが入っていない Entity なら INSERT、主キーが入っている Entity なら UPDATE を実行します。データベースから取り出した Entity には社員ID(主キー)が入っているので、UPDATE になります。新規登録(第11章)と同じ save メソッドで、登録にも更新にも使えるのです。
変更が終わったあと、ブラウザの「戻る」で確認画面に戻って、もう一度「変更」を押すと、Whitelabel エラー画面になります。この教材では、この操作への対処はしていません。
12.7 よくあるエラー
| 症状 | 主な原因と対処 |
|---|---|
| Whitelabel エラー画面(400 Bad Request) | URL の社員ID の部分が数字になっていません。@{...} の書き方を確認してください |
| Whitelabel エラー画面(404 Not Found) |
@GetMapping/@PostMapping の URL と、テンプレートの送信先 URL が一致しているか確認してください |
| 変更画面で部署のプルダウンが「選択してください」になっている |
input メソッドで updateForm.setDeptId(employee.getDepartment().getDeptId()) を書き忘れていないか確認してください |
12.8 まとめと確認問題
まとめ
- 変更画面では、
findByEmpIdで取り出した Entity の値をフォームに詰め替えて表示する - どの社員を変更するかは、URL の
{empId}で表し、@PathVariableで受け取る -
saveは、主キーが入っている Entity なら UPDATE を実行する - 変更の保存時は、データベースから社員を取り出し直して値を上書きする
確認問題
問1 変更画面を開いたとき、入力欄に今のデータが入っているのはなぜですか。
解答を見る
input メソッドで、findByEmpId でデータベースから取り出した社員の Entity の値を UpdateForm に詰め替え、そのフォームを Model に入れているからです。th:field は、フォームに入っている値を入力欄に表示します。
問2 @GetMapping("/employees/{empId}/edit") のメソッドに /employees/4/edit でアクセスしたとき、@PathVariable Integer empId には何が入りますか。
解答を見る
4 が入ります。URL の {empId} の部分の値が、引数に渡されます。
問3 新規登録でも変更でも、同じ save メソッドを使っています。INSERT になるか UPDATE になるかは、何で決まりますか。
解答を見る
Entity に主キー(社員ID)が入っているかどうかで決まります。入っていなければ INSERT、入っていれば UPDATE になります。





