Skip to content
Playground

11.詳細・編集・削除

学習目標

  • @PathVariable で URL のパスから本の id を受け取れる
  • findByIdoptional() で 1 冊を取得し、見つからない場合を扱える
  • th:objectth:field で既存値を埋めた編集フォームを作れる
  • UPDATE・DELETE を JdbcClient で実行できる
  • 一覧・詳細・編集・削除で CRUD を完成させ、各操作が 3 層を貫くことを確認できる

データの基本操作は、作成(Create)・取得(Read)・更新(Update)・削除(Delete)の 4 つで、頭文字をとって CRUD と呼びます。JdbcClient までに、登録(Create)と一覧・検索による取得(Read)を実装しました。本章で、1 冊の詳細表示(Read)・編集(Update)・削除(Delete)を加え、CRUD をそろえます。

一覧から 1 冊を選び、その詳細を表示する画面を作ります。

一覧は全件を並べるだけで、個々の本を区別する必要がありませんでした。1 冊を取り出すには、どの本かを識別する値が要ります。それが、books テーブルの主キー id です。取得した本を id で扱えるよう、Bookid を加えます。

Book.java
package com.example.bookstore;
public class Book {
private final Long id;
private final String title;
private final int price;
private final int stock;
public Book(Long id, String title, int price, int stock) {
this.id = id;
this.title = title;
this.price = price;
this.stock = stock;
}
public Long getId() {
return id;
}
public String getTitle() {
return title;
}
public int getPrice() {
return price;
}
public int getStock() {
return stock;
}
}

id の型は Long です。

一覧から詳細へ移れるよう、findAll の SELECT に id を加え、各行に詳細へのリンクを張ります。

BookRepository.java(findAll に id を追加)
public List<Book> findAll() {
return jdbcClient.sql("SELECT id, title, price, stock FROM books")
.query(Book.class)
.list();
}

books.html の一覧で、タイトルを詳細へのリンクにします。

templates/books.html(一覧の行)
<tr th:each="book : ${books}" th:class="${book.stock == 0} ? 'sold-out' : ''">
<td><a th:href="@{/books/{id}(id=${book.id})}" th:text="${book.title}">こころ</a></td>
<td><span th:text="${book.price}">473</span></td>
<td th:text="${book.stock}">5</td>
</tr>

th:href はリンク先を設定する属性です。@{...}URL 式 で、/books/{id}{id}(id=${book.id}) で本の id を埋めます。id が 1 の本なら、リンク先は /books/1 になります。

詳細画面の URL は /books/1 のように、パスに id を含めます。パスの一部を受け取るには @PathVariable を使います。

@GetMapping("/books/{id}")
public String メソッド名(@PathVariable Long id, ...) { ... }

@GetMapping("/books/{id}"){id} が変わる部分で、同じ名前の引数 @PathVariable Long id がその値を受け取ります。/books/1 なら id1 が入ります。

クエリパラメーター を受け取る @RequestParam との違いは、値の位置です。

アノテーション値の位置
@PathVariableパスの一部/books/1
@RequestParamクエリ(? 以降)/books?keyword=猫

一覧を絞る検索条件はクエリ、特定の 1 件を指す id はパス、と使い分けます。

BookRepository に、id で 1 冊を取り出す findById を加えます。

BookRepository.java(findById を追加)
public Optional<Book> findById(Long id) {
return jdbcClient.sql("SELECT id, title, price, stock FROM books WHERE id = :id")
.param("id", id)
.query(Book.class)
.optional();
}

1 つの値の取得 で使った single() は、結果がちょうど 1 件のときのものでした。詳細では、指定した id の本があるとは限りません。0 件か 1 件かを受け取るのが optional() で、戻り値は Optional<Book> です。本があれば中身を持ち、なければ空になります。java.util.Optionalimport が加わります。

BookServicefindByIdBookRepository に委譲します。

BookService.java(findById を追加)
public Optional<Book> findById(Long id) {
return bookRepository.findById(id);
}

BookController で、id を @PathVariable で受け取り、本を取得します。

BookController.java(detail を追加)
@GetMapping("/books/{id}")
public String detail(@PathVariable Long id, Model model) {
Optional<Book> bookOpt = bookService.findById(id);
if (bookOpt.isEmpty()) {
return "redirect:/books";
}
model.addAttribute("book", bookOpt.get());
return "books/detail";
}

bookOpt.isEmpty()Optional が空か(本が見つからなかったか)を判定します。空なら一覧へリダイレクトし、本があれば bookOpt.get()Optional から本を取り出して Model に渡します。

詳細画面を表示する books/detail.html です。

templates/books/detail.html
<!DOCTYPE html>
<html lang="ja" xmlns:th="http://www.thymeleaf.org">
<head th:replace="~{fragments/layout :: head('本の詳細')}"></head>
<body>
<h1 th:text="${book.title}">こころ</h1>
<p>価格: <span th:text="${book.price}">473</span></p>
<p>在庫: <span th:text="${book.stock}">5</span></p>
<a th:href="@{/books/{id}/edit(id=${book.id})}">編集</a>
</body>
</html>

一覧のタイトルをクリックすると、その本の詳細が表示されます。

詳細画面の「編集」から、本の内容を更新できるようにします。

編集は、既存の値をフォームに表示してから修正します。本の登録 のフォームは空の入力欄でしたが、編集では各欄に現在の値を埋めておきます。

入力欄に値を埋めるには、フォームを対象のオブジェクトに結びつけます。th:object がフォーム全体を 1 つのオブジェクトに、th:field が各入力欄をそのフィールドに結びつけます。

<form th:object="${オブジェクト}">
<input th:field="*{フィールド名}">
</form>

th:field="*{title}" は、th:object のオブジェクトの title を入力欄に結びつけ、nameidvalue を自動で生成します。value には現在の値が入るため、既存の値が表示されます。

たとえば、title"こころ" を入れた BookForm を渡すと、th:field="*{title}" の入力欄は次の HTML を生成します。

<input type="text" id="title" name="title" value="こころ">

value="こころ" が入るため、フォームを開くと入力欄に既存のタイトルが表示されます。

編集フォームを表示する処理を BookController に加えます。先ほどは値を直接入れましたが、実際にはデータベースから本を取得し、その値を入れた BookForm をテンプレートに渡します。

BookController.java(editForm を追加)
@GetMapping("/books/{id}/edit")
public String editForm(@PathVariable Long id, Model model) {
Optional<Book> bookOpt = bookService.findById(id);
if (bookOpt.isEmpty()) {
return "redirect:/books";
}
Book book = bookOpt.get();
BookForm form = new BookForm();
form.setTitle(book.getTitle());
form.setPrice(book.getPrice());
model.addAttribute("bookForm", form);
model.addAttribute("bookId", id);
return "books/edit";
}

取得した本の titlepriceBookForm に入れ、bookForm という名前で渡します。id は入力欄ではなく送信先の URL に使うため、BookForm には入れず、bookId として別に渡します。

templates/books/edit.html
<!DOCTYPE html>
<html lang="ja" xmlns:th="http://www.thymeleaf.org">
<head th:replace="~{fragments/layout :: head('本の編集')}"></head>
<body>
<h1>本の編集</h1>
<form th:action="@{/books/{id}(id=${bookId})}" method="post" th:object="${bookForm}">
<label for="title">タイトル:</label>
<input type="text" th:field="*{title}">
<label for="price">価格:</label>
<input type="number" th:field="*{price}">
<button type="submit">更新</button>
</form>
</body>
</html>

th:action="@{/books/{id}(id=${bookId})}" で、送信先を /books/1 のように本の id を含む URL にします。フォームを開くと、各欄に既存のタイトルと価格が表示されます。

フォームの送信先は /books/{id} への POST です。id を @PathVariable、入力値を @ModelAttribute で受け取ります。

BookRepository に、UPDATE で本を更新する update を加えます。

BookRepository.java(update を追加)
public void update(Long id, String title, int price) {
jdbcClient.sql("UPDATE books SET title = :title, price = :price WHERE id = :id")
.param("title", title)
.param("price", price)
.param("id", id)
.update();
}

WHERE id = :id で、更新する 1 行を id で指定します。在庫は編集しないので、更新するのは titleprice です。

BookServiceupdate を加えます。

BookService.java(update を追加)
public void update(Long id, BookForm form) {
bookRepository.update(id, form.getTitle(), form.getPrice());
}

BookController で、/books/{id} への POST を受け取ります。

BookController.java(update を追加)
@PostMapping("/books/{id}")
public String update(@PathVariable Long id, @ModelAttribute BookForm form) {
bookService.update(id, form);
return "redirect:/books";
}

更新後は リダイレクト で一覧へ戻します。/books/1/edit でタイトルや価格を修正して更新すると、一覧にその変更が反映されます。

削除は、データを消す副作用のある操作です。フォーム送信 で取得には GET、データの送信には POST を使いました。削除も POST で送ります。リンク(GET)で削除できると、ページを開いただけで本が消えるおそれがあるためです。

詳細画面に、ボタンだけを送る削除フォームを置きます。

templates/books/detail.html(削除フォームを追加)
<form th:action="@{/books/{id}/delete(id=${book.id})}" method="post">
<button type="submit">削除</button>
</form>

/books/{id}/delete への POST で、その本を削除します。

BookRepository に、DELETE で本を消す deleteById を加えます。

BookRepository.java(deleteById を追加)
public void deleteById(Long id) {
jdbcClient.sql("DELETE FROM books WHERE id = :id")
.param("id", id)
.update();
}

DELETE FROM books WHERE id = :id で、id で指定した 1 行を削除します。INSERT・UPDATE と同じく、DELETE も update() で実行します。

BookServicedelete を加えます。

BookService.java(delete を追加)
public void delete(Long id) {
bookRepository.deleteById(id);
}

BookController で、/books/{id}/delete への POST を受け取ります。

BookController.java(delete を追加)
@PostMapping("/books/{id}/delete")
public String delete(@PathVariable Long id) {
bookService.delete(id);
return "redirect:/books";
}

削除後は一覧へリダイレクトします。詳細画面で削除ボタンを押すと、その本が消え、一覧に戻ります。

これで、本の作成・取得・更新・削除がそろいました。

操作HTTP メソッドパスRepository
一覧(Read)GET/booksfindAll
詳細(Read)GET/books/{id}findById
登録(Create)POST/bookssave
編集フォームGET/books/{id}/editfindById
更新(Update)POST/books/{id}update
削除(Delete)POST/books/{id}/deletedeleteById

どの操作も、BookController(ルーティング)→ BookService(窓口)→ BookRepository(SQL)の レイヤードアーキテクチャ を貫きます。操作を加えても、各層に同じ役割でメソッドが増えるだけで、構成は変わりません。