第10章で作った確認画面に「戻る」「登録」のボタンを付け、完了画面を作ります。「登録」ボタンで社員がデータベースに登録されるところまで完成させましょう。
この章で学ぶこと
- 確認画面から入力画面に「戻る」しくみ
- フォームの内容を Entity に詰め替えて、データベースに登録する方法
- 登録が終わったらセッションの入力内容を破棄する方法
- PRG パターンで二重登録を防ぐ方法
11.1 この章で作るもの
この章が終わると、新規登録は次のように動くようになります。

図11-1 新規登録の画面の流れ(URL の先頭の /employees/register は省略)
この章で追加・作成するのは、次のものです。
| 種類 | ファイル | 内容 |
|---|---|---|
| Controller | EmployeeRegisterController.java |
戻る・登録・完了の3つの処理を追加する |
| テンプレート | employee/register/confirm.html |
第10章で作った確認画面に、「戻る」「登録」のボタンを追加する |
| テンプレート | employee/register/complete.html |
完了画面(新規作成) |
Controller に追加する処理と URL の対応は次のとおりです。
| URL | メソッド | Java のメソッド | 処理 |
|---|---|---|---|
/employees/register/back |
GET | back |
確認画面から入力画面に戻る |
/employees/register/complete |
POST | complete |
社員を登録し、完了画面へリダイレクトする |
/employees/register/done |
GET | done |
完了画面を表示する |
11.2 Controller に処理を追加する
第10章で作った EmployeeRegisterController を開き、3つのメソッド(back、complete、done)を追加します。
- パッケージ・エクスプローラーで
src/main/java→com.example.employeeapp.controller→EmployeeRegisterController.javaをダブルクリックして開きます。 - 次のコードのとおりに、
import文と3つのメソッド(back、complete、done)を追加します。@PostMappingやSessionStatusなどを補完(Ctrl + Space)で入力すれば、import文は自動で追加されます(第4章「入力は補完で行う」)。 - Ctrl + S で保存します。
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.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.RegisterForm;
import com.example.employeeapp.repository.DepartmentRepository;
import com.example.employeeapp.repository.EmployeeRepository;
@Controller
@SessionAttributes("registerForm")
public class EmployeeRegisterController {
@Autowired
private EmployeeRepository employeeRepository;
@Autowired
private DepartmentRepository departmentRepository;
/** セッションに RegisterForm オブジェクトがないとき呼ばれる
* @return 空の RegisterForm オブジェクト */
@ModelAttribute
public RegisterForm createForm() {
return new RegisterForm();
}
/** 社員登録の入力画面を表示
* @param model 表示する入力フォームと部署一覧
* @return 社員登録の入力画面 */
@GetMapping("/employees/register")
public String input(Model model) {
model.addAttribute("registerForm", new RegisterForm());
List<Department> departments = departmentRepository.findAllByOrderByDeptIdAsc();
model.addAttribute("departments", departments);
return "employee/register/input";
}
/** 社員登録の確認画面を表示
* @param registerForm 入力された社員情報
* @param model 表示する部署情報
* @return 社員登録の確認画面 */
@PostMapping("/employees/register/confirm")
public String confirm(@ModelAttribute RegisterForm registerForm, Model model) {
Department department = departmentRepository.findByDeptId(registerForm.getDeptId());
model.addAttribute("department", department);
return "employee/register/confirm";
}
/** 社員登録の確認画面から入力画面に戻る
* @param model 表示する部署一覧
* @return 社員登録の入力画面 */
@GetMapping("/employees/register/back")
public String back(Model model) {
List<Department> departments = departmentRepository.findAllByOrderByDeptIdAsc();
model.addAttribute("departments", departments);
return "employee/register/input";
}
/** 社員登録の完了画面を表示
* @param registerForm 入力された社員情報
* @param sessionStatus セッション破棄時に利用
* @return 社員登録の完了画面 */
@PostMapping("/employees/register/complete")
public String complete(@ModelAttribute RegisterForm registerForm,
SessionStatus sessionStatus) {
Employee employee = new Employee();
employee.setName(registerForm.getName());
employee.setGender(registerForm.getGender());
employee.setAge(registerForm.getAge());
employee.setBirthDate(registerForm.getBirthDate());
employee.setDepartment(departmentRepository.findByDeptId(registerForm.getDeptId()));
employee.setAddress(registerForm.getAddress());
employeeRepository.save(employee);
// セッションに保存していた入力内容を破棄する
sessionStatus.setComplete();
return "redirect:/employees/register/done";
}
/** 社員登録の完了画面を表示
* @return 社員登録の完了画面 */
@GetMapping("/employees/register/done")
public String done() {
return "employee/register/complete";
}
}
【知識メモ】3つのメソッドの役割
-
back… 入力画面をもう一度表示する。入力内容はセッションに残っているので、フォームを作り直す必要はない -
complete… フォームの内容を Entity に詰め替えてデータベースに登録し、完了画面へリダイレクトする -
done… 完了画面を表示するだけ
11.3 確認画面にボタンを付け、完了画面を作る
確認画面(confirm.html)にボタンを付ける
第10章で作った templates/employee/register/confirm.html を開き、</table> の後ろに「戻る」と「登録」のボタンを追加して保存します。追加するのは、<a class="button" …>戻る</a> から </form> までの4行です。
<!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="${registerForm}">
<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/register/back}">戻る</a>
<form class="inline" th:action="@{/employees/register/complete}" method="post">
<button type="submit">登録</button>
</form>
</body>
</html>
完了画面(complete.html)
同じ register フォルダーに 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>
これで、新規登録に必要なファイルがすべてそろいました。register フォルダーの中は次のようになっています。
templates
└─ employee
├─ list.html
└─ register
├─ input.html … 入力画面(第10章)
├─ confirm.html … 確認画面(第10章。この章でボタンを追加)
└─ complete.html … 完了画面(この章)
11.4 動作確認
アプリを起動している場合は、コンソールの赤い四角(終了)ボタンで一度停止してから、プロジェクトを右クリック → 実行 → Spring Boot アプリケーション で起動し直します(Boot ダッシュボードの再起動ボタンでもかまいません)。停止している場合は、そのまま起動します。保存しただけでは、動いているアプリには反映されません。
確認画面を表示する
-
Chrome で
http://localhost:8080/employeesを開き、「新規登録」ボタンを押します。 -
入力画面に次の内容を入力します。
項目 入力する値 氏名 田中 美咲 性別 女 年齢 24 生年月日 2001/06/15 部署 開発部 住所(都道府県) 愛知県 -
「確認」ボタンを押します。第10章の確認画面に「戻る」「登録」のボタンが付いていれば成功です。
「戻る」で入力画面に戻る
- 確認画面の「戻る」ボタンを押します。
- 入力画面に戻り、さきほど入力した内容がそのまま残っている ことを確認します。

図11-3 確認画面から戻った入力画面(入力内容が残っている)
登録する
- もう一度「確認」ボタンを押して確認画面に進み、「登録」ボタンを押します。
- 完了画面が表示されれば成功です。アドレス欄が
/employees/register/doneになっていることも確認してください。
このとき Eclipse のコンソールには、Hibernate が実行した SQL が表示されています。insert into employee の SQL があれば、データベースに登録された証拠です。
Hibernate:
select
seq_emp.nextval
from
dual
Hibernate:
insert
into
employee
(address, age, birth_date, dept_id, gender, name, emp_id)
values
(?, ?, ?, ?, ?, ?, ?)
insert の前に、select seq_emp.nextval from dual が実行されていることに注目してください。JPA がシーケンス SEQ_EMP から次の番号を取り出し、その番号を社員ID(emp_id)にして insert しています(第3章・第6章)。? の部分には、取り出した番号とフォームに入力した値が入ります。
一覧画面で確認する
- 完了画面の「一覧へ戻る」をクリックします。
- 一覧の最後に、田中 美咲さんが追加されていれば成功です。
【知識メモ】社員IDが 4 になる理由
社員ID 3 の林檎 星飛さんは第9章で削除したため、一覧にはありません。シーケンスは一度払い出した番号を再利用しないので、新しい社員には次の番号の 4 が振られます。番号が飛んでいても問題ありません。
11.5 解説
「戻る」で入力内容が残るしくみ
第10章で見たように、@ModelAttribute を付けた引数には、セッションに保存されたフォームに送信された値を詰めたものが渡されます。この章では、そのフォームを「戻る」と「登録」でも使い回します。

図11-6 セッションのフォームを、確認・戻る・登録で使い回す
back メソッドは、フォームを何も用意せずに入力画面を返しています。
@GetMapping("/employees/register/back")
public String back(Model model) {
List<Department> departments = departmentRepository.findAllByOrderByDeptIdAsc();
model.addAttribute("departments", departments);
return "employee/register/input";
}
@SessionAttributes を付けた Controller では、セッションに保存されているフォームが自動的に Model に入ります。そのため入力画面の th:object="${registerForm}" には、セッションのフォーム、つまり入力済みの内容が表示されます。
一方、最初に入力画面を開くときの input メソッドでは、new RegisterForm() で空のフォームを Model に入れて、セッションの内容を上書きしています。そのため、新規登録ボタンから開いたときは、いつも空の入力画面になります。
| メソッド | Model の registerForm | 入力画面の表示 |
|---|---|---|
input(新規登録ボタン) |
new RegisterForm() で空のフォームに入れ替える |
空欄 |
back(戻るボタン) |
セッションのフォームがそのまま入る | 入力済みの内容 |
確認画面のボタン
「戻る」は入力画面を表示するだけなので、第10章の「新規登録」ボタンと同じように、リンク(class="button")で作っています。「登録」はデータを変えるので、POST のフォームです。<form> は通常、次の行に分かれて表示されます。そこで class="inline" を付けて、「戻る」の横に並ぶようにしています(app.css の form.inline。第9章で「削除」ボタンのフォームに付けたものと同じです)。
フォームから Entity への詰め替え
データベースに登録するのは Entity(Employee)です。フォーム(RegisterForm)は画面の入力を受け取るためのクラスなので、そのままでは保存できません。そこで complete メソッドでは、フォームの値を1つずつ Entity に詰め替えています。
Employee employee = new Employee();
employee.setName(registerForm.getName());
employee.setGender(registerForm.getGender());
employee.setAge(registerForm.getAge());
employee.setBirthDate(registerForm.getBirthDate());
employee.setDepartment(departmentRepository.findByDeptId(registerForm.getDeptId()));
employee.setAddress(registerForm.getAddress());
employeeRepository.save(employee);
- 社員IDはセットしません。社員IDは、保存するときに JPA がシーケンスから番号を取り出して自動で採番します(第3章・第6章)。
-
部署は、部署IDではなく Department オブジェクトをセットします。
Employeeは部署をDepartment型で持っている(第6章の@ManyToOne)ので、部署IDから部署を検索してセットします。 -
saveは、主キーが null なら登録(INSERT)します。社員IDをセットしていないので、新しい社員として登録されます。
【知識メモ】フォームと Entity を分ける理由
フォームは「画面の都合」に合わせたクラス、Entity は「テーブルの都合」に合わせたクラスです。たとえば画面では部署を部署IDで選びますが、Entity は Department オブジェクトで持っています。役割の違うクラスを分けておくと、画面かテーブルの一方を変えたときに、もう一方への影響を小さくできます。
sessionStatus.setComplete() でセッションを片付ける
登録が終わったら、セッションに保存していた入力内容はもう必要ありません。sessionStatus.setComplete() を呼ぶと、この Controller が @SessionAttributes で保存していた registerForm がセッションから削除されます。
SessionStatus は、メソッドの引数に書くだけで Spring が渡してくれます。
PRG パターンで二重登録を防ぐ
complete メソッドは完了画面を直接返さずに、redirect:/employees/register/done でリダイレクトしています。これは第9章の削除でも使った PRG パターン(Post → Redirect → Get)です。
| 作り方 | 完了画面で再読み込み(F5)すると |
|---|---|
| POST のあと完了画面を直接返す | ブラウザが「登録」の POST をもう一度送るので、同じ社員がもう一人登録される |
| POST のあとリダイレクトする(PRG) | ブラウザは最後の GET(/done)を送り直すだけなので、完了画面が表示されるだけ
|
実際に完了画面で F5 を押して、一覧に同じ社員が増えないことを確かめてみましょう。
登録が終わったあと、ブラウザの「戻る」で確認画面に戻って、もう一度「登録」を押すと、Whitelabel エラー画面になります。この教材では、この操作への対処はしていません。
11.6 入力チェックはまだありません
今のアプリには、まだ入力チェックがありません。試しに、入力画面で 何も入力せずに「確認」→「登録」と進んでみましょう。
【ご注意】何も入力せずに登録するとエラーになる
確認画面は空欄のまま表示されますが、「登録」を押すと Whitelabel エラー画面(ステータス 500)になります。コンソールには次のようなエラーが表示されます。
ORA-01400: ("EMPAPP"."EMPLOYEE"."NAME")にはNULLは挿入できません。
第3章で EMPLOYEE テーブルの列に NOT NULL を付けたため、データベースが空の値の登録を拒否したのです。本来は、データベースに送る前にアプリ側で入力内容をチェックし、利用者にわかりやすいメッセージで知らせるべきです。これを第13章で作ります。
11.7 よくあるエラー
| 症状 | 原因 | 対処 |
|---|---|---|
| 「戻る」を押すと 405 になる |
back メソッドを @PostMapping にしてしまった |
@GetMapping("/employees/register/back") にする(リンクは GET でリクエストを送る) |
| 「登録」を押すと 405 になる | 確認画面の「登録」のフォームが method="get" になっている |
「登録」のフォームは method="post" にする |
| 完了画面で F5 を押すと同じ社員が増える |
complete メソッドで redirect: を付けずに完了画面を直接返している |
戻り値の先頭に redirect: を付けて、/done へリダイレクトする |
| 新規登録ボタンで開いた入力画面に、前回の内容が残っている |
setComplete() を呼び忘れた、または input メソッドで空のフォームを Model に入れていない |
この章のコードと見比べる |
11.8 まとめと確認問題
まとめ
- 確認画面から「戻る」と、セッションのフォームがそのまま表示されるので、入力内容が残る
- データベースに登録するときは、フォームの値を Entity に詰め替えて
saveする。社員IDはデータベースが採番する - 登録が終わったら
sessionStatus.setComplete()でセッションの入力内容を破棄する - POST のあとはリダイレクトして完了画面を表示する(PRG パターン)ことで、二重登録を防ぐ
- 今はまだ入力チェックがないので、空欄のまま登録するとデータベースのエラーになる(第13章で対応)
確認問題
問1 確認画面から「戻る」ボタンを押すと、入力画面に入力内容が残っているのはなぜですか。
解答を見る
入力内容を入れたフォーム(registerForm)が @SessionAttributes によってセッションに保存されていて、back メソッドで入力画面を表示するときに、そのフォームが自動的に Model に入るからです。
問2 complete メソッドで、Entity に社員IDをセットしないのはなぜですか。
解答を見る
社員IDは、保存するときに JPA がシーケンスから番号を取り出して自動で採番するからです。主キーが null のまま save すると、新しいデータとして登録(INSERT)されます。
問3 sessionStatus.setComplete() は何をするメソッドですか。
解答を見る
その Controller が @SessionAttributes でセッションに保存していた値(ここでは registerForm)を、セッションから削除します。登録が終わって入力内容が不要になったときに呼びます。
問4 complete メソッドで完了画面のテンプレートを直接返さず、redirect:/employees/register/done にしているのはなぜですか。
解答を見る
完了画面で再読み込みされたときに、「登録」の POST がもう一度送られて二重登録になるのを防ぐためです(PRG パターン)。リダイレクトすると、ブラウザが最後に送ったのは GET(/done)になるので、再読み込みしても完了画面が表示されるだけです。


