いよいよ社員管理アプリの画面を作ります。第7章で作った Repository を使ってデータベースから社員を取り出し、Thymeleaf で一覧表として表示します。
この章で学ぶこと
- Controller に Repository を注入して、データベースのデータを画面に渡す方法
- Thymeleaf の
th:eachで、リストの件数分だけ行を繰り返す方法 - 画面に表示しやすい形にデータを変換する方法(性別・日付・部署名)
- CSS などのファイルを置く
staticフォルダーの役割
8.1 この章で作るもの
この章が終わると、Chrome で http://localhost:8080/employees を開いたときに、次のような社員一覧が表示されるようになります。表示されているのは、第3章でデータベースに登録した3人の社員です。
「新規登録」「変更」「削除」のボタンは、第9章以降で順番に追加していきます。この章では、一覧を表示するところまでを作ります。
この章で作成・変更するファイルは次のとおりです。
| ファイル | 作業 | 内容 |
|---|---|---|
controller/EmployeeListController.java |
新規 | 社員一覧画面を表示する Controller |
static/css/app.css |
新規 | 画面の見た目を整える CSS |
templates/employee/list.html |
新規 | 社員一覧画面のテンプレート |
図8-2 社員一覧画面が表示されるまでの流れ
8.2 EmployeeListController を作る
次に、社員一覧画面を表示する Controller を作ります。
-
src/main/java→com.example.employeeapp.controllerパッケージ(第4章で作成)を右クリックし、新規 → クラス を選びます。 - 「名前」に
EmployeeListControllerと入力し、完了 をクリックします。 - エディターが開くので、次のように入力して保存します。
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 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";
}
}
第5章で予告したとおり、@Autowired で EmployeeRepository を受け取る 形になっています。new を書いていないのに employeeRepository が使えるのは、Spring が第7章で作った EmployeeRepository の実体(Bean)を注入してくれるからです。
【知識メモ】MyBatis コースの場合
MyBatis コースでは、EmployeeRepository の代わりに、第7章 MyBatis 入門②で作った EmployeeMapper を受け取ります。上のコードの次の3か所を書き換えます。ほかは同じです。
import com.example.employeeapp.mapper.EmployeeMapper;
@Autowired
private EmployeeMapper employeeMapper;
List<Employee> employees = employeeMapper.findAllByOrderByEmpIdAsc();
この先の章のコードでも、MyBatis コースでは、EmployeeRepository・employeeRepository を EmployeeMapper・employeeMapper に、DepartmentRepository・departmentRepository を DepartmentMapper・departmentMapper に読み替えます。import 文は com.example.employeeapp.repository.… を com.example.employeeapp.mapper.… にします。メソッドの名前は同じです。読み替えだけでは済まないところには、その章に「MyBatis コースの場合」の囲みを置いています。
【再確認】クラス名は補完で入力する
List や Employee などのクラス名、@GetMapping などのアノテーションは、途中まで打って Ctrl + Space で補完すると、import 文も自動で追加されます(第4章「入力は補完で行う」)。List のように同じ名前のクラスが複数ある場合は、List - java.util を選んでください。
8.3 CSS ファイルを作る
表に罫線を付けるなど、画面の見た目を整えるための CSS ファイルを作ります。CSS ファイルは src/main/resources/static フォルダーの中に置きます。
-
src/main/resources→staticフォルダーを右クリックし、新規 → フォルダー を選びます。 - 「フォルダー名」に
cssと入力し、完了 をクリックします。 - 作成した
cssフォルダーを右クリックし、新規 → ファイル を選びます。 - 「ファイル名」に
app.cssと入力し、完了 をクリックします。 - エディターが開くので、次のように入力して保存します。
body { /* ページ全体 */
margin: 24px;
color: #222;
}
h1 { /* 見出し */
font-size: 24px;
}
table { /* 表 */
border-collapse: collapse;
margin-bottom: 16px;
}
th, td { /* 表のマス */
border: 1px solid #999;
padding: 6px 12px;
text-align: left;
}
th { /* 表の見出しのマス */
background-color: #eee;
}
a.button { /* リンクをボタンの見た目にする(新規登録・変更・戻るボタン用) */
display: inline-block;
padding: 1px 6px;
border: 1px solid #767676;
border-radius: 3px;
background-color: #efefef;
color: #000;
font-size: 13px;
text-decoration: none;
}
form.inline { /* フォームを横に並べる(削除ボタン用) */
display: inline;
}
.error { /* 入力チェックのエラーメッセージ */
color: #c00;
font-size: 14px;
}
この app.css は、HTML入門 第10章 で作った app.css と同じ内容です(Eclipse の「ファイル」で作ったので、先頭の @charset の行はありません)。セレクターとプロパティの意味は、HTML入門 7.8「解説:セレクター」 と 7.9「解説:プロパティ」 を参照してください。
最後の a.button、form.inline、.error は、この章ではまだ使いません。a.button はリンクをボタンの見た目にするため(第10章以降)、form.inline はボタンを横に並べるため(第9章以降)、.error は入力チェックのエラーメッセージを赤く表示するため(第13章以降)に使います。
static フォルダーの役割
static フォルダーに置いたファイルは、Spring Boot が そのまま ブラウザに送ります。CSS や画像、JavaScript のファイルなど、内容が変わらないファイルを置く場所です。
URL は、static フォルダーの中の場所がそのまま使われます。今回作ったファイルは、http://localhost:8080/css/app.css という URL でアクセスできます。
| フォルダー | 置くもの | ブラウザへの送られ方 |
|---|---|---|
templates |
Thymeleaf のテンプレート(HTML) | Controller から呼ばれ、データが埋め込まれてから送られる |
static |
CSS、画像、JavaScript など | ファイルの中身がそのまま送られる |
8.4 一覧画面のテンプレートを作る
Controller の list メソッドは、"employee/list" を返しています。これは templates/employee/list.html を使う、という意味でした(第4章)。まず employee フォルダーを作り、その中にテンプレートを作ります。
-
src/main/resources→templatesフォルダーを右クリックし、新規 → フォルダー を選びます。 - 「フォルダー名」に
employeeと入力し、完了 をクリックします。 - 作成した
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>
</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>
</tr>
</table>
</body>
</html>
th: で始まる属性を除けば、HTML入門 10.3「一覧画面を作る」 で作った社員一覧と同じ HTML です。CSS を読み込む <link rel="stylesheet"> は HTML入門 7.7「CSS の読み込み」、表のタグは HTML入門 6.5「解説:表のタグ」、性別のマスの <span> は HTML入門 4.7「div と span」 を参照してください。href の代わりに th:href を書いている理由は、8.6 で解説します。
【ご注意】フォルダーの場所に注意
list.html は、templates の 中の employee フォルダーに作ってください。templates の直下に作ると、Controller が返す "employee/list" と場所が合わず、エラーになります。
8.5 動作確認
- アプリを起動している場合は、コンソールの赤い四角(終了)ボタンで一度停止してから、プロジェクトを右クリック → 実行 → Spring Boot アプリケーション で起動し直します(Boot ダッシュボードの再起動ボタンでもかまいません)。停止している場合は、そのまま起動します。保存しただけでは、動いているアプリには反映されません。
- コンソールに
Started EmployeeAppApplicationと表示されたら、Chrome でhttp://localhost:8080/employeesを開きます。
第3章で登録した3人の社員が、社員IDの順に表示されれば成功です。性別が「男」「女」、生年月日が「2000/06/22」の形式、部署が部署名で表示されていることを確認してください。
【便利な使い方】データベースと見比べてみよう
SQL Developer で第3章の結合 SELECT を実行し、画面に表示された内容と同じであることを確かめてみましょう。アプリはデータベースの内容をそのまま表示しているので、SQL Developer でデータを変更して COMMIT すると、Chrome で画面を再読み込みしたときに一覧にも反映されます。
8.6 解説
Controller の処理
list メソッドがしていることは、次の3つだけです。
-
employeeRepository.findAllByOrderByEmpIdAsc()で、全社員を社員IDの順に取り出す(第7章で作ったメソッド) -
model.addAttribute("employees", employees)で、取り出した社員のリストにemployeesという名前を付けて Model に入れる -
"employee/list"を返して、templates/employee/list.htmlで画面を作るよう指示する
データベースからデータを取り出す処理は Repository に、画面の見た目はテンプレートに任せ、Controller は「何を取り出して、どの画面に渡すか」だけを決めています。第1章で学んだ MVC の役割分担のとおりです。
Thymeleaf の書き方
| 書き方 | 意味 |
|---|---|
th:each="employee : ${employees}" |
Model の employees(社員のリスト)から1人ずつ取り出して employee という名前を付け、その要素(ここでは <tr>)を人数分繰り返す。Java の拡張 for 文 for (Employee employee : employees) と同じ考え方 |
th:text="${employee.name}" |
タグの中身を、employee の name の値に置き換える |
th:switch="${employee.gender}"th:case="1"
|
th:switch に書いた値(性別)と同じ th:case の要素だけを表示する。1 なら「男」、2 なら「女」が表示される |
${employee.department.deptName} |
社員が所属する部署の部署名 |
${#dates.format(employee.birthDate, 'yyyy/MM/dd')} |
日付(Date)を「2000/06/22」の形式の文字に変換する |
th:href="@{/css/app.css}" |
アプリの中の URL を作る。@{…} は URL を表す書き方 |
${employee.name} は getter を呼んでいる
${employee.name} と書くと、Thymeleaf は employee.getName() を呼び出して値を取り出します。フィールドを直接見ているのではなく、「get + 先頭を大文字にした名前」のメソッド を呼んでいます。
| テンプレートの書き方 | 呼び出されるメソッド |
|---|---|
${employee.empId} |
employee.getEmpId() |
${employee.gender} |
employee.getGender() |
${employee.department.deptName} |
employee.getDepartment().getDeptName() |
【知識メモ】名前の決まりで動く
Spring と Thymeleaf は、名前の決まりで動くところが多くあります。たとえば、Controller が返す "employee/list" は templates/employee/list.html に、model.addAttribute("employees", …) の employees はテンプレートの ${employees} に、${employee.name} の name は getName() に対応します。
つづりや大文字・小文字が1文字でも違うと、動きません。エラーにならずに、表の中身が空のまま表示されることもあります(8.8 よくあるエラー)。動かないときは、まず名前を見比べてください。
部署名が取れる仕組み
社員テーブルには部署IDしか入っていませんが、画面には部署名が表示されています。これは第6章で、Employee クラスに @ManyToOne を付けた Department 型のフィールドを持たせたからです。
JPA は社員を取り出すときに、部署IDをもとに部署のデータも一緒に取り出し、employee.getDepartment() で使えるようにしてくれます。第3章で SQL Developer から実行した「社員テーブルと部署テーブルの結合」と同じことを、JPA が自動で行っているのです。
【知識メモ】MyBatis コースの場合
MyBatis コースでは、部署名を取り出す仕組みが違います。第7章 MyBatis 入門②で、社員テーブルと部署テーブルを INNER JOIN で結合する SQL を Mapper の XML に書き、resultMap の association で、部署の列の値を Employee の department フィールドに入れるようにしました。そのため、employee.getDepartment() で部署を取り出せます。結合は MyBatis が自動で行うのではなく、自分で書いた SQL で行っています。
8.7 コンソールで SQL を確認する
第4章で application.properties に spring.jpa.show-sql=true を設定したので、JPA が実行した SQL がコンソールに表示されます。Chrome で一覧画面を開いたあと、Eclipse のコンソールを見てみましょう。
Hibernate:
select
e1_0.emp_id,
e1_0.address,
e1_0.age,
e1_0.birth_date,
e1_0.dept_id,
e1_0.gender,
e1_0.name
from
employee e1_0
order by
e1_0.emp_id
Hibernate:
select
d1_0.dept_id,
d1_0.dept_name
from
department d1_0
where
d1_0.dept_id=?
(部署を取り出す select が、社員が所属する部署の数だけ続く)
まず社員テーブル(employee)を社員IDの順に取り出し、続いて部署テーブル(department)から、社員が所属する部署を1件ずつ取り出していることがわかります。表示される SQL の細かい形は、バージョンなどによって変わることがあります。
【知識メモ】MyBatis コースの場合
MyBatis コースでは、第4章で logging.level.com.example.employeeapp.mapper=debug を設定したので、Mapper が実行した SQL が次のように表示されます(行の先頭の日時などは省略しています。表示の読み方は 第7章 MyBatis 入門② の 7.4)。
c.e.e.m.E.findAllByOrderByEmpIdAsc : ==> Preparing: SELECT e.EMP_ID, e.NAME, e.GENDER, e.AGE, e.BIRTH_DATE, e.ADDRESS, d.DEPT_ID, d.DEPT_NAME FROM EMPLOYEE e INNER JOIN DEPARTMENT d ON e.DEPT_ID = d.DEPT_ID ORDER BY e.EMP_ID
c.e.e.m.E.findAllByOrderByEmpIdAsc : ==> Parameters:
c.e.e.m.E.findAllByOrderByEmpIdAsc : <== Total: 3
第7章で XML に書いた SQL が、そのまま1回だけ実行されています。社員と部署を結合して取り出しているので、JPA コースのように部署を1件ずつ取り出す SQL は続きません。下の「現場では」の囲みは JPA コースの話ですが、MyBatis でも、どんな SQL が何回実行されているかをコンソールで確かめる習慣は同じように役に立ちます。
【知識メモ】SQL を書かなくても SQL は動いている
私たちは SQL を1行も書いていませんが、裏側ではこのように SQL が実行されています。うまく動かないときは、コンソールの SQL を見ると原因がわかることがあります。
【ご注意】現場では
この教材のデータは数件なので問題になりませんが、上のように「社員を取り出したあと、部署を1件ずつ取り出す」動きは、データ量が多い実際のシステムでは SQL の回数が増え、性能に大きく影響することがあります。現場では、どんな SQL が何回実行されているかを確認し、必要に応じてチームで取り出し方を検討するようにしましょう。
8.8 よくあるエラー
| 症状 | 原因 | 対処 |
|---|---|---|
500 エラーになり、コンソールに TemplateInputException と表示される |
Controller が返すテンプレート名と、テンプレートの場所・ファイル名が合っていない |
return "employee/list"; と、templates/employee/list.html の場所・つづりを確認する |
| 表の見出しだけが表示され、社員が1人も表示されない |
${employees} のつづりが、Controller の model.addAttribute("employees", …) の名前と違う |
Controller とテンプレートで同じ名前になっているか確認する |
500 エラーになり、Property or field 'xxx' cannot be found と表示される |
テンプレートに書いた名前に対応する getter がない(例:${employee.gendername} と小文字で書いた) |
名前のつづりと大文字・小文字を、Entity の getter 名と見比べる |
| 表に罫線が付かない(CSS が効いていない) |
app.css の場所が違う、または th:href のパスが違う |
src/main/resources/static/css/app.css にあるか、th:href="@{/css/app.css}" と書いたかを確認する。Chrome で http://localhost:8080/css/app.css を開いて中身が表示されるかも確かめる |
| 404 エラー(Whitelabel エラー画面)になる | URL の打ち間違い、または @GetMapping の URL の間違い |
Chrome の URL が /employees になっているか、@GetMapping("/employees") と一致しているか確認する。送られた URL は 付録D の方法で確かめられる |
その他のエラーは、付録B「画面表示時のエラー」 も参考にしてください。
【知識メモ】MyBatis コースの場合
MyBatis コースのアプリは、この章で社員一覧を表示するときに初めてデータベースに接続します(第4章)。そのため、接続設定の間違いは、起動のときではなくここで分かります。一覧を表示したときに 500 エラーになり、コンソールに ORA-01017(ユーザー名またはパスワードの間違い)や ORA-12514・ORA-12541(接続先の間違い)が表示されたら、第4章の「よくあるエラー」のとおり application.properties の接続設定を確認します。
Mapper や XML の書き間違いによるエラー(Invalid bound statement (not found) など)は、第7章 MyBatis 入門② の 7.5にまとめています。
8.9 まとめと確認問題
まとめ
- Controller は
@Autowiredで Repository を受け取り、取り出したデータを Model に入れてテンプレートに渡す -
th:eachを使うと、リストの件数分だけ HTML の要素を繰り返せる -
${employee.name}は、getter のgetName()を呼び出して値を取り出す - 性別の 1・2 は、
th:switchとth:caseで「男」「女」に切り替えて表示する -
@ManyToOneのおかげで、${employee.department.deptName}と書くだけで部署名を表示できる - 日付は
#dates.formatで表示用の形式に変換できる - CSS や画像は
staticフォルダーに置くと、そのままブラウザに送られる
確認問題
問1 Controller の list メソッドが "employee/list" を返すと、どのファイルが画面の作成に使われますか。
解答を見る
src/main/resources/templates/employee/list.html です。templates/ と拡張子 .html は自動で補われます。
問2 th:each="employee : ${employees}" は、Java のどんな書き方に近いですか。
解答を見る
拡張 for 文 for (Employee employee : employees) { … } に近い書き方です。リストから1人ずつ取り出して、要素を人数分繰り返します。
問3 社員テーブルの性別には 1・2 の数値が入っています。一覧画面で「男」「女」と表示するために、テンプレートで何を使いましたか。
解答を見る
th:switch と th:case です。th:switch="${employee.gender}" の値が 1 なら th:case="1" の「男」が、2 なら th:case="2" の「女」が表示されます。
問4 社員テーブルには部署IDしか入っていないのに、一覧画面に部署名を表示できるのはなぜですか。
解答を見る
Employee クラスが @ManyToOne で Department 型のフィールドを持っているため、JPA が部署IDをもとに部署のデータも取り出してくれるからです。テンプレートでは ${employee.department.deptName} と書くだけで部署名を取り出せます。MyBatis コースでは、Mapper の XML の SQL で部署テーブルを結合し、resultMap の association で部署のデータを Employee の department フィールドに入れているからです。
問5 src/main/resources/static/css/app.css に置いたファイルには、どの URL でアクセスできますか。
解答を見る
http://localhost:8080/css/app.css です。static フォルダーの中の場所が、そのまま URL になります。


