Memo

メモ > 技術 > フレームワーク: SpringBoot > 引き続き

■引き続き
■データの登録編集削除表示(機能の実装) 以下のテーブルを作成し、登録編集削除表示の各機能を作成してみる
CREATE TABLE authors( id INT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '代理キー', created DATETIME NOT NULL COMMENT '作成日時', modified DATETIME NOT NULL COMMENT '更新日時', code VARCHAR(255) NOT NULL UNIQUE COMMENT 'コード', name VARCHAR(255) NOT NULL COMMENT '名前', kana VARCHAR(255) COMMENT 'カナ', PRIMARY KEY(id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT '著者';
src/main/java/com/example/demo/entity/Author.java を新規に作成
package com.example.demo.entity; import jakarta.persistence.*; import lombok.Data; import java.util.Date; @Entity @Table(name="authors") @Data public class Author { @Id @GeneratedValue(strategy=GenerationType.IDENTITY) private Integer id; @Column(nullable = false, updatable = false) private Date created; @Column(nullable = false) private Date modified; @Column(unique = true, nullable = false) private String code; @Column(nullable = false) private String name; private String kana; }
src/main/java/com/example/demo/request/AuthorSearchRequest.java を新規に作成
package com.example.demo.request; import lombok.Data; import java.io.Serializable; @Data public class AuthorSearchRequest implements Serializable { private String code; private String name; }
src/main/java/com/example/demo/request/AuthorSaveRequest.java を新規に作成
package com.example.demo.request; import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.Size; import lombok.Data; import java.io.Serializable; @Data public class AuthorSaveRequest implements Serializable { private int id; @NotEmpty(message = "コードを入力してください。") @Size(max = 20, message = "コードは20文字以内で入力してください。") private String code; @NotEmpty(message = "名前を入力してください。") @Size(max = 80, message = "名前は80文字以内で入力してください。") private String name; @Size(max = 80, message = "カナは80文字以内で入力してください。") private String kana; }
src/main/java/com/example/demo/request/AuthorDeleteRequest.java を新規に作成
package com.example.demo.request; import lombok.Data; import java.io.Serializable; @Data public class AuthorDeleteRequest implements Serializable { private int id; }
src/main/java/com/example/demo/repository/AuthorRepository.java を新規に作成
package com.example.demo.repository; import com.example.demo.entity.Author; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; @Repository public interface AuthorRepository extends JpaRepository<Author, Integer> { Page<Author> findByNameLikeOrKanaLike(String name, String kana, Pageable pageable); boolean existsByCode(String code); boolean existsByCodeAndIdNot(String code, Integer id); }
【Spring Data JPA】自動実装されるメソッドの命名ルール - Qiita https://qiita.com/shindo_ryo/items/af7d12be264c2cc4b252 src/main/java/com/example/demo/service/AuthorService.java を新規に作成
package com.example.demo.service; import com.example.demo.entity.Author; import com.example.demo.repository.AuthorRepository; import com.example.demo.request.AuthorSaveRequest; import lombok.AllArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.validation.BindingResult; import org.springframework.validation.FieldError; import java.util.Date; import java.util.List; @Service @AllArgsConstructor public class AuthorService { private final AuthorRepository authorRepository; public List<Author> select() { List<Author> authors = authorRepository.findAll(); return authors; } public Page<Author> selectByPage(Pageable pageable) { Page<Author> authors = authorRepository.findAll(pageable); return authors; } public Author selectById(int id) { Author author = authorRepository.findById(id).get(); return author; } public void create(AuthorSaveRequest authorSaveRequest) { Author author = new Author(); author.setId(null); author.setCreated(new Date()); author.setModified(new Date()); author.setCode(authorSaveRequest.getCode()); author.setName(authorSaveRequest.getName()); author.setKana(authorSaveRequest.getKana()); authorRepository.save(author); } public void update(AuthorSaveRequest authorSaveRequest) { Author author = new Author(); author.setId(authorSaveRequest.getId()); author.setModified(new Date()); author.setCode(authorSaveRequest.getCode()); author.setName(authorSaveRequest.getName()); author.setKana(authorSaveRequest.getKana()); authorRepository.saveAndFlush(author); } public void deleteById(int id) { authorRepository.deleteById(id); } public Page<Author> searchByPage(String name, Pageable pageable) { Page<Author> authors = authorRepository.findByNameLikeOrKanaLike("%" + name + "%", "%" + name + "%", pageable); return authors; } public boolean isValid(AuthorSaveRequest authorSaveRequest, BindingResult result) { boolean valid = true; if (authorSaveRequest.getId() == 0) { if (authorRepository.existsByCode(authorSaveRequest.getCode())) { result.addError(new FieldError(result.getObjectName(), "code", "すでに登録されています。")); valid = false; } } else { if (authorRepository.existsByCodeAndIdNot(authorSaveRequest.getCode(), authorSaveRequest.getId())) { result.addError(new FieldError(result.getObjectName(), "code", "すでに登録されています。")); valid = false; } } return valid; } }
src/main/resources/templates/author/index.html を新規に作成
<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{layout/frontend}"> <head> <title>著者 | Demo</title> </head> <body> <th:block layout:fragment="content"> <main> <p><a href="/author/form">著者を追加</a></p> <form th:action="@{/author/}" th:object="${authorSearchRequest}" method="get"> <dl> <dt>名前</dt> <dd><input type="text" size="20" name="name" th:value="*{name}"></dd> </dl> <p><input type="submit" value="検索"></p> </form> <table> <tr> <th>ID</th> <th>コード</th> <th>名前</th> <th>カナ</th> </tr> <tr th:each="author : ${authors}"> <td><a th:href="@{/author/view/__${author.id}__}">[[${author.id}]]</a></td> <td>[[${author.code}]]</td> <td>[[${author.name}]]</td> <td>[[${author.kana}]]</td> </tr> </table> <div> <p th:text="|全${authors.getTotalPages()}ページ中${authors.getNumber() + 1}ページ目を表示中|"></p> <ul> <li> <span th:if="${authors.isFirst()}">&lt;前</span> <a th:unless="${authors.isFirst()}" th:href="@{/author/(name=${authorSearchRequest.name}, page=${authors.getNumber() - 1})}">&lt;前</a> </li> <li th:each="i : ${#numbers.sequence(0, authors.getTotalPages() - 1)}"> <span th:if="${i == authors.getNumber()}" th:text="${i + 1}"></span> <a th:if="${i != authors.getNumber()}" th:href="@{/author/(name=${authorSearchRequest.name}, page=${i})}" th:text="${i + 1}"></a> </li> <li> <span th:if="${authors.isLast()}">次&gt;</span> <a th:unless="${authors.isLast()}" th:href="@{/author/(name=${authorSearchRequest.name}, page=(${authors.getNumber() + 1}))}">次&gt;</a> </li> </ul> </div> </main> </th:block> </body> </html>
「__${author.id}__」は値を埋め込むための書き方 これで「author.id」が「1」なら「/author/view/1」というURLが作成される src/main/resources/templates/author/form.html を新規に作成
<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{layout/frontend}"> <head> <title>著者 | Demo</title> </head> <body> <th:block layout:fragment="content"> <main> <p>著者を登録します。</p> <form th:action="@{/author/form}" th:object="${authorSaveRequest}" method="post"> <dl> <dt>コード</dt> <dd><input type="text" name="code" size="10" th:value="*{code}"><div th:if="${#fields.hasErrors('code')}" th:errors="*{code}" class="error">Code Error</div></dd> <dt>名前</dt> <dd><input type="text" name="name" size="20" th:value="*{name}"><div th:if="${#fields.hasErrors('name')}" th:errors="*{name}" class="error">Name Error</div></dd> <dt>カナ</dt> <dd><input type="text" name="kana" size="20" th:value="*{kana}"><div th:if="${#fields.hasErrors('kana')}" th:errors="*{kana}" class="error">Kana Error</div></dd> </dl> <p><input type="submit" value="登録"></p> </form> </main> </th:block> </body> </html>
src/main/resources/templates/author/view.html を新規に作成
<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{layout/frontend}"> <head> <title>著者 | Demo</title> </head> <body> <th:block layout:fragment="content"> <main> <p>著者を編集します。</p> <form th:action="@{/author/view/__*{id}__}" th:object="${authorSaveRequest}" method="post"> <input type="hidden" name="id" th:value="*{id}"> <dl> <dt>コード</dt> <dd><input type="text" name="code" size="10" th:value="*{code}"><div th:if="${#fields.hasErrors('code')}" th:errors="*{code}" class="error">Code Error</div></dd> <dt>名前</dt> <dd><input type="text" name="name" size="20" th:value="*{name}"><div th:if="${#fields.hasErrors('name')}" th:errors="*{name}" class="error">Name Error</div></dd> <dt>カナ</dt> <dd><input type="text" name="kana" size="20" th:value="*{kana}"><div th:if="${#fields.hasErrors('kana')}" th:errors="*{kana}" class="error">Kana Error</div></dd> </dl> <p><input type="submit" value="編集"></p> </form> <p>著者を削除します。</p> <form th:action="@{/author/delete/__*{id}__}" th:object="${authorSaveRequest}" method="post"> <input type="hidden" th:field="*{id}"> <p><input type="submit" value="削除"></p> </form> </main> </th:block> </body> </html>
src/main/java/com/example/demo/controller/AuthorController.java を新規に作成
package com.example.demo.controller; import com.example.demo.entity.Author; import com.example.demo.request.AuthorDeleteRequest; import com.example.demo.request.AuthorSaveRequest; import com.example.demo.request.AuthorSearchRequest; import com.example.demo.service.AuthorService; import lombok.AllArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.web.PageableDefault; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.validation.BindingResult; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; @Controller @AllArgsConstructor public class AuthorController { private final AuthorService authorService; @GetMapping(value = "/author/") String index(@ModelAttribute AuthorSearchRequest authorSearchRequest, @PageableDefault(page = 0, size = 5, sort = "id") Pageable pageable, Model model) { if (authorSearchRequest.getName() == null) { Page<Author> authors = authorService.selectByPage(pageable); model.addAttribute("authors", authors); } else { Page<Author> authors = authorService.searchByPage(authorSearchRequest.getName(), pageable); model.addAttribute("authors", authors); } return "author/index"; } @GetMapping(value = "/author/form") String form(Model model) { model.addAttribute("authorSaveRequest", new Author()); return "author/form"; } @PostMapping(value = "/author/form") String form(@Validated @ModelAttribute AuthorSaveRequest authorSaveRequest, BindingResult result) { boolean valid = authorService.isValid(authorSaveRequest, result); if (result.hasErrors() || !valid) { return "author/form"; } authorService.create(authorSaveRequest); return "redirect:/author/"; } @GetMapping(value = "/author/view/{id}") String view(@PathVariable("id") int id, Model model) { Author author = authorService.selectById(id); model.addAttribute("authorSaveRequest", author); return "author/view"; } @PostMapping(value = "/author/view/{id}") String view(@Validated @ModelAttribute AuthorSaveRequest authorSaveRequest, BindingResult result) { boolean valid = authorService.isValid(authorSaveRequest, result); if (result.hasErrors() || !valid) { return "author/view"; } authorService.update(authorSaveRequest); return "redirect:/author/"; } @PostMapping(value = "/author/delete/{id}") String delete(@ModelAttribute AuthorDeleteRequest authorDeleteRequest) { authorService.deleteById(authorDeleteRequest.getId()); return "redirect:/author/"; } }
表示確認用に、authors テーブルには適当にデータを登録しておく また SecurityConfig.java を調整して、/author/ はログインを求めないようにしておく ※重複エラーのバリデーションはアノテーションで対応できるようにするか ■データの登録編集削除表示(ページャーの実装について補足)
String index(@ModelAttribute AuthorSearchRequest authorSearchRequest, Model model) { List<Author> authors = authorRepository.findAll();
上記のようなコードを以下のようにすると、ページャーを利用できる
String index(@ModelAttribute AuthorSearchRequest authorSearchRequest, @PageableDefault(page = 0, size = 5, sort = "id") Pageable pageable, Model model) { Page<Author> authors = authorRepository.findAll(pageable);
ページャーは、一例だが以下のようなコードで表示できる
<div> <span th:text="|全${authors.getTotalPages()}ページ中${authors.getNumber() + 1}ページ目を表示中|"></span> <ul> <li> <span th:if="${authors.isFirst()}">&lt;前</span> <a th:unless="${authors.isFirst()}" th:href="@{/author/(page=${authors.getNumber() - 1})}">&lt;前</a> </li> <li th:each="i : ${#numbers.sequence(0, authors.getTotalPages() - 1)}"> <span th:if="${i == authors.getNumber()}" th:text="${i + 1}"></span> <a th:if="${i != authors.getNumber()}" th:href="@{/author/(page=${i})}" th:text="${i + 1}"></a> </li> <li> <span th:if="${authors.isLast()}">次&gt;</span> <a th:unless="${authors.isLast()}" th:href="@{/author/(page=(${authors.getNumber() + 1}))}">次&gt;</a> </li> </ul> </div>
■データの登録編集削除表示(テンプレートのvalueとfieldについて補足) 入力画面の「name="code" th:value="*{code}"」は「th:field="*{code}」の状態だと、 サービスでの独自入力チェックでエラーになったとき、入力内容を保持できなかった ■データの登録編集削除表示(登録画面と編集画面のURLについて補足) 以下のように、入力画面と登録実行時のURLを同じものにしている
@GetMapping(value = "/author/form") String form(Model model) {
@PostMapping(value = "/author/form") String form(@Validated @ModelAttribute AuthorSaveRequest authorSaveRequest, BindingResult result) {
■データの登録編集削除表示(コンストラクタインジェクションについて補足) コントローラーで @Autowired を使うのはフィールドインジェクションという方法 この場合、以下のような記述になる
@Controller public class AuthorController { @Autowired private AuthorService authorService;
ただし現在はコンストラクタインジェクションが推奨されている
@Controller public class AuthorController { private final AuthorService authorService; @Autowired AuthorController(AuthorService authorService) { this.authorService = authorService; }
しかしこれだとフィールドインジェクションに比べて行数が増える そこで「コンストラクタが1つしかない場合、@Autowired は省略できる」というSpringBootのルールを利用する またこのコンストラクタは、クラスの全インスタンス(1つしか無いが)に変数をセットしている この場合、@AllArgsConstructor を使うことでコンストラクタも省略できる
@Controller @AllArgsConstructor public class AuthorController { private final AuthorService authorService;
これで今回のコードと同じになる ■データの紐づけ(紐づけ前の準備) 上記が作成できたら以下のテーブルを作成し、記事に対して著者を登録できるようにするか
CREATE TABLE entries( id INT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '代理キー', created DATETIME NOT NULL COMMENT '作成日時', modified DATETIME NOT NULL COMMENT '更新日時', datetime DATETIME NOT NULL COMMENT '日時', title VARCHAR(255) NOT NULL COMMENT 'タイトル', text TEXT COMMENT '本文', author_id INT UNSIGNED NOT NULL COMMENT '外部キー 著者', PRIMARY KEY(id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT '記事';
src/main/java/com/example/demo/entity/Entry.java を新規に作成
package com.example.demo.entity; import jakarta.persistence.*; import lombok.Data; import java.util.Date; @Entity @Table(name="entries") @Data public class Entry { @Id @GeneratedValue(strategy=GenerationType.IDENTITY) private Integer id; @Column(nullable = false, updatable = false) private Date created; @Column(nullable = false) private Date modified; @Column(nullable = false) private Date datetime; @Column(nullable = false) private String title; private String text; @Column(name = "author_id", nullable = false) private Integer authorId; }
src/main/java/com/example/demo/request/EntrySearchRequest.java を新規に作成
package com.example.demo.request; import lombok.Data; import java.io.Serializable; @Data public class EntrySearchRequest implements Serializable { private String keyword; }
src/main/java/com/example/demo/request/EntrySaveRequest.java を新規に作成
package com.example.demo.request; import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.Size; import lombok.Data; import java.beans.ConstructorProperties; import java.io.Serializable; @Data public class EntrySaveRequest implements Serializable { private int id; @NotEmpty(message = "日時を入力してください。") private String datetime; @NotEmpty(message = "タイトルを入力してください。") @Size(max = 80, message = "タイトルは80文字以内で入力してください。") private String title; @NotEmpty(message = "本文を入力してください。") private String text; @NotEmpty(message = "著者を入力してください。") private String authorId; @ConstructorProperties({"author_id"}) public EntrySaveRequest(String author_id) { // フォームから「name="author_id"」で送られた値を「authorId」という名前で扱う this.authorId = author_id; } }
SpringBootでスネークケースのリクエストパラメータを受け取る方法 - エキサイト TechBlog. https://tech.excite.co.jp/entry/2021/10/07/082811 src/main/java/com/example/demo/request/EntryDeleteRequest.java を新規に作成
package com.example.demo.request; import lombok.Data; import java.io.Serializable; @Data public class EntryDeleteRequest implements Serializable { private int id; }
src/main/java/com/example/demo/repository/EntryRepository.java を新規に作成
package com.example.demo.repository; import com.example.demo.entity.Entry; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; @Repository public interface EntryRepository extends JpaRepository<Entry, Integer> { Page<Entry> findByTitleLikeOrTextLike(String title, String text, Pageable pageable); }
src/main/java/com/example/demo/service/EntryService.java を新規に作成
package com.example.demo.service; import com.example.demo.entity.Entry; import com.example.demo.repository.EntryRepository; import com.example.demo.request.EntrySaveRequest; import lombok.AllArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.validation.BindingResult; import org.springframework.validation.FieldError; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Date; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; @Service @AllArgsConstructor public class EntryService { private final EntryRepository entryRepository; public List<Entry> select() { List<Entry> entries = entryRepository.findAll(); return entries; } public Page<Entry> selectByPage(Pageable pageable) { Page<Entry> entries = entryRepository.findAll(pageable); return entries; } public Entry selectById(int id) { Entry entry = entryRepository.findById(id).get(); return entry; } public void create(EntrySaveRequest entrySaveRequest) { SimpleDateFormat sdFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss"); Date datetime = null; try { datetime = sdFormat.parse(entrySaveRequest.getDatetime()); } catch (ParseException e) { throw new RuntimeException(e); } Entry entry = new Entry(); entry.setId(null); entry.setCreated(new Date()); entry.setModified(new Date()); entry.setDatetime(datetime); entry.setTitle(entrySaveRequest.getTitle()); entry.setText(entrySaveRequest.getText()); entry.setAuthorId(Integer.valueOf(entrySaveRequest.getAuthorId())); entryRepository.save(entry); } public void update(EntrySaveRequest entrySaveRequest) { SimpleDateFormat sdFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss"); Date datetime = null; try { datetime = sdFormat.parse(entrySaveRequest.getDatetime()); } catch (ParseException e) { throw new RuntimeException(e); } Entry entry = new Entry(); entry.setId(entrySaveRequest.getId()); entry.setModified(new Date()); entry.setDatetime(datetime); entry.setTitle(entrySaveRequest.getTitle()); entry.setText(entrySaveRequest.getText()); entry.setAuthorId(Integer.valueOf(entrySaveRequest.getAuthorId())); entryRepository.saveAndFlush(entry); } public void deleteById(int id) { entryRepository.deleteById(id); } public Page<Entry> search(String keyword, Pageable pageable) { Page<Entry> entries = entryRepository.findByTitleLikeOrTextLike("%" + keyword + "%", "%" + keyword + "%", pageable); return entries; } public boolean isValid(EntrySaveRequest entrySaveRequest, BindingResult result) { boolean valid = true; Pattern pattern = Pattern.compile("^\\d\\d\\d\\d-\\d\\d-\\d\\d \\d\\d:\\d\\d:\\d\\d$"); Matcher matcher = pattern.matcher(entrySaveRequest.getDatetime()); if (!matcher.find()) { result.addError(new FieldError(result.getObjectName(), "datetime", "日時の形式が不正です。")); valid = false; } return valid; } }
src/main/resources/templates/entry/index.html を新規に作成
<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{layout/frontend}"> <head> <title>記事 | Demo</title> </head> <body> <th:block layout:fragment="content"> <main> <p><a href="/entry/form">記事を追加</a></p> <form th:action="@{/entry/}" th:object="${entrySearchRequest}" method="get"> <dl> <dt>キーワード</dt> <dd><input type="text" size="20" name="keyword" th:value="*{keyword}"></dd> </dl> <p><input type="submit" value="検索"></p> </form> <table> <tr> <th>ID</th> <th>日時</th> <th>タイトル</th> <th>本文</th> </tr> <tr th:each="entry : ${entries}"> <td><a th:href="@{/entry/view/__${entry.id}__}">[[${entry.id}]]</a></td> <td>[[${#dates.format(entry.datetime, 'yyyy-MM-dd HH:mm:ss')}]]</td> <td>[[${entry.title}]]</td> <td>[[${entry.text}]]</td> </tr> </table> <div> <p th:text="|全${entries.getTotalPages()}ページ中${entries.getNumber() + 1}ページ目を表示中|"></p> <ul> <li> <span th:if="${entries.isFirst()}">&lt;前</span> <a th:unless="${entries.isFirst()}" th:href="@{/entry/(keyword=${entrySearchRequest.keyword}, page=${entries.getNumber() - 1})}">&lt;前</a> </li> <li th:each="i : ${#numbers.sequence(0, entries.getTotalPages() - 1)}"> <span th:if="${i == entries.getNumber()}" th:text="${i + 1}"></span> <a th:if="${i != entries.getNumber()}" th:href="@{/entry/(keyword=${entrySearchRequest.keyword}, page=${i})}" th:text="${i + 1}"></a> </li> <li> <span th:if="${entries.isLast()}">次&gt;</span> <a th:unless="${entries.isLast()}" th:href="@{/entry/(keyword=${entrySearchRequest.keyword}, page=(${entries.getNumber() + 1}))}">次&gt;</a> </li> </ul> </div> </main> </th:block> </body> </html>
src/main/resources/templates/entry/form.html を新規に作成
<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{layout/frontend}"> <head> <title>記事 | Demo</title> </head> <body> <th:block layout:fragment="content"> <main> <p>記事を登録します。</p> <form th:action="@{/entry/form}" th:object="${entrySaveRequest}" method="post"> <dl> <dt>日時</dt> <dd><input type="text" name="datetime" size="20" th:value="*{datetime} ? *{datetime} : ${#dates.format(#dates.createNow(), 'yyyy-MM-dd HH:mm:ss')}"><div th:if="${#fields.hasErrors('datetime')}" th:errors="*{datetime}" class="error">Datetime Error</div></dd> <dt>タイトル</dt> <dd><input type="text" name="title" size="20" th:value="*{title}"><div th:if="${#fields.hasErrors('title')}" th:errors="*{title}" class="error">Title Error</div></dd> <dt>本文</dt> <dd><textarea name="text" rows="10" cols="50">[[*{text}]]</textarea><div th:if="${#fields.hasErrors('text')}" th:errors="*{text}" class="error">Text Error</div></dd> <dt>著者</dt> <dd> <select name="author_id"> <option value=""></option> <option th:each="author : ${authors}" th:value="${author.id}" th:text="${author.name}" th:selected="${author.id} == *{authorId}"> </option> </select> <div th:if="${#fields.hasErrors('authorId')}" th:errors="*{authorId}" class="error">AuthorId Error</div> </dd> </dl> <p><input type="submit" value="登録"></p> </form> </main> </th:block> </body> </html>
【Spring Boot】Thymeleafでプルダウンを作成する方法 - ITを分かりやすく解説 https://medium-company.com/thymeleaf-%E3%83%97%E3%83%AB%E3%83%80%E3%82%A6%E3%83%B3/ src/main/resources/templates/entry/view.html を新規に作成
<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{layout/frontend}"> <head> <title>記事 | Demo</title> </head> <body> <th:block layout:fragment="content"> <main> <p>記事を編集します。</p> <form th:action="@{/entry/view/__*{id}__}" th:object="${entrySaveRequest}" method="post"> <input type="hidden" name="id" th:value="*{id}"> <dl> <dt>日時</dt> <dd><input type="text" name="datetime" size="20" th:value="*{#strings.replace(datetime, '.0', '')}"><div th:if="${#fields.hasErrors('datetime')}" th:errors="*{datetime}" class="error">Datetime Error</div></dd> <dt>タイトル</dt> <dd><input type="text" name="title" size="20" th:value="*{title}"><div th:if="${#fields.hasErrors('title')}" th:errors="*{title}" class="error">Title Error</div></dd> <dt>本文</dt> <dd><textarea name="text" rows="10" cols="50">[[*{text}]]</textarea><div th:if="${#fields.hasErrors('text')}" th:errors="*{text}" class="error">Text Error</div></dd> <dt>著者</dt> <dd> <select name="author_id"> <option value=""></option> <option th:each="author : ${authors}" th:value="${author.id}" th:text="${author.name}" th:selected="${author.id} == *{authorId}"> </option> </select> <div th:if="${#fields.hasErrors('authorId')}" th:errors="*{authorId}" class="error">AuthorId Error</div> </dd> </dl> <p><input type="submit" value="編集"></p> </form> <p>記事を削除します。</p> <form th:action="@{/entry/delete/__*{id}__}" th:object="${entrySaveRequest}" method="post"> <input type="hidden" th:field="*{id}"> <p><input type="submit" value="削除"></p> </form> </main> </th:block> </body> </html>
src/main/java/com/example/demo/controller/EntryController.java を新規に作成
package com.example.demo.controller; import com.example.demo.entity.Entry; import com.example.demo.request.EntryDeleteRequest; import com.example.demo.request.EntrySaveRequest; import com.example.demo.request.EntrySearchRequest; import com.example.demo.service.AuthorService; import com.example.demo.service.EntryService; import lombok.AllArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.web.PageableDefault; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.validation.BindingResult; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; @Controller @AllArgsConstructor public class EntryController { private final EntryService entryService; private final AuthorService authorService; @GetMapping(value = "/entry/") String index(@ModelAttribute EntrySearchRequest entrySearchRequest, @PageableDefault(page = 0, size = 5, sort = "id") Pageable pageable, Model model) { if (entrySearchRequest.getKeyword() == null) { Page<Entry> entries = entryService.selectByPage(pageable); model.addAttribute("entries", entries); } else { Page<Entry> entries = entryService.search(entrySearchRequest.getKeyword(), pageable); model.addAttribute("entries", entries); } return "entry/index"; } @GetMapping(value = "/entry/form") String form(Model model) { model.addAttribute("entrySaveRequest", new Entry()); model.addAttribute("authors", authorService.select()); return "entry/form"; } @PostMapping(value = "/entry/form") String form(@Validated @ModelAttribute EntrySaveRequest entrySaveRequest, BindingResult result, Model model) { boolean valid = entryService.isValid(entrySaveRequest, result); if (result.hasErrors() || !valid) { model.addAttribute("authors", authorService.select()); return "entry/form"; } entryService.create(entrySaveRequest); return "redirect:/entry/"; } @GetMapping(value = "/entry/view/{id}") String view(@PathVariable("id") int id, Model model) { Entry entry = entryService.selectById(id); model.addAttribute("entrySaveRequest", entry); model.addAttribute("authors", authorService.select()); return "entry/view"; } @PostMapping(value = "/entry/view/{id}") String view(@Validated @ModelAttribute EntrySaveRequest entrySaveRequest, BindingResult result, Model model) { boolean valid = entryService.isValid(entrySaveRequest, result); if (result.hasErrors() || !valid) { model.addAttribute("authors", authorService.select()); return "entry/view"; } entryService.update(entrySaveRequest); return "redirect:/entry/"; } @PostMapping(value = "/entry/delete/{id}") String delete(@ModelAttribute EntryDeleteRequest entryDeleteRequest) { entryService.deleteById(entryDeleteRequest.getId()); return "redirect:/entry/"; } }
■データの紐づけ(紐づけの実装) EntryからAuthorを参照できるように、またその逆も参照できるようにする src/main/java/com/example/demo/entity/Entry.java
@ManyToOne @JoinColumn(name = "author_id", insertable = false, updatable = false) private Author author;
src/main/java/com/example/demo/entity/Author.java
@OneToMany(mappedBy = "author") @ToString.Exclude private List<Entry> entities;
@JoinColumn の「insertable = false, updatable = false」については以下を参照 「@Column と @JoinColumn の両方が定義されているとき、『この項目は参照専用に定義したもの』と明示する」ためのもの 今回の場合「private Integer authorId;」と「@JoinColumn(name = "author_id")」の両方があると、「author_idという列が2つある」と解釈されて意図したように更新できない可能性がある JPA @ManyToOne時の@JoinColumnのパラメータ、insertable = false,updatable = falseについて https://teratail.com/questions/113950 また「@ToString.Exclude」が無いと、toString() を呼び出したときに循環参照になって「java.lang.StackOverflowError」が発生する これで相互にデータを参照できる プログラムから参照する場合、一例だが以下のようなコードになる
List<Author> authors = authorService.select(); for (Author author : authors) { logger.info(author.getName()); logger.info(String.valueOf(author.getEntities().size())); logger.info(author.toString()); } List<Entry> entries = entryService.select(); for (Entry entry : entries) { logger.info(entry.getTitle()); logger.info(entry.getAuthor().getName()); logger.info(entry.toString()); }
記事一覧に著者名を表示する場合、一例だが以下のようなコードになる src/main/resources/templates/entry/index.html
<td>[[${entry.author.name}]]</td>
著者一覧に記事数を表示する場合、一例だが以下のようなコードになる src/main/resources/templates/author/index.html
<td>[[${#lists.size(author.entities)}]]件</td>
なお、実行時に以下のようなエラーになった場合、 Table [entries] contains physical column name [author_id] referred to by multiple logical column names: [author_id], [authorId] Entityを以下のように調整すれば解消するかもしれない
@Column(nullable = false) private Integer authorId; ↓ @Column(name = "author_id", nullable = false) private Integer authorId;
java - A column in a table is referred to by multiple physical column names - Stack Overflow https://stackoverflow.com/questions/57691377/a-column-in-a-table-is-referred-to-by-multiple-physical... ■データの紐づけ(多対多) 上記が作成できたら以下のテーブルを作成し、記事に対して複数の分類を登録できるようにするか
CREATE TABLE categories( id INT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '代理キー', created DATETIME NOT NULL COMMENT '作成日時', modified DATETIME NOT NULL COMMENT '更新日時', name VARCHAR(255) NOT NULL COMMENT '名前', sort INT UNSIGNED NOT NULL COMMENT '並び順', PRIMARY KEY(id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT '分類'; CREATE TABLE category_sets( category_id INT UNSIGNED NOT NULL COMMENT '外部キー 分類', member_id INT UNSIGNED NOT NULL COMMENT '外部キー 記事' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT '分類 ひも付け';

Advertisement