- Eclipse(2023-03)
- OpenJDK 20.0.1
- Spring Boot 3.1.1
- MySQL(MariaDB) - 10.11.3
- MySQL Workbench - 8.0.33
- diary.zip
-
Spring BootでWebアプリを作る題材として、ブラウザ上で動作する簡単な日記投稿Webアプリを作成します。日記の新規投稿だけでなく、編集、削除、一覧表示という基本的なCRUD機能を備えたものを作成します。
-
また、今回はデータベースにMySQLを使用します。
- C -> Create(データの作成)
- R -> Read(データの取得)
- U -> Update(データの更新)
- D -> Delete(データの削除)
- 割愛
- Spring Bootのプロジェクトのインポートが完了したらアプリを起動します。この状態で起動しても、何も表示されずエラーになりますが、ちゃんと起動するかの動作確認です。
DiaryApplication.java
というJavaファイルが自動生成されていると思いますのでこのファイルを右クリックし→実行→Spring Bootを選択すると起動します。 - デフォルトではhttp://localhost:8080/でアクセスできるはずです。
-
データベースの作成
-- diarydbの作成 create database diarydb; use diarydb; -- Tableの作成 create table diary(id integer auto_increment,body_text varchar(255) not null, create_datetime timestamp not null,primary key(id)); -- データのInsert insert into diary (body_text,create_datetime) values ('I am very happy!',LOCALTIME()); -- データの確認 select * from diary;
-
以下のような結果が返ってくればOK
id body_text create_datetime 1 I am very happy! 2023-07-03T10:32:10 -
接続ユーザ追加
-- ユーザ名:diary_user,パスワード:P@ssWordで作成する grant all on *.* to diary_user identified by 'P@ssWord'; select user,host,password from mysql.user;
-
以下のような結果が返ってくればOK(省略あり
User Host Password diary_user % *X9DAKAP+D86A...
- 以下を記述します。
# DB接続 spring.datasource.url=jdbc:mysql://localhost/diarydb spring.datasource.username=diary_user spring.datasource.password=P@ssWord
- Diaryテーブルを想定して、Javaのエンティティクラス
Diary.java
を作成します。 com.example.diary.models
パッケージを作成し、その中にDiary.java
を作成します。package com.example.diary.models; import java.time.LocalDateTime; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; @Entity public class Diary { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private int id; @Column(name = "body_text", nullable = false) private String bodyText; @Column(name = "create_datetime", nullable = false) private LocalDateTime createDatetime; public Diary(){} public Diary(int id, String bodyText, LocalDateTime createDatetime) { this.id = id; this.bodyText = bodyText; this.createDatetime = createDatetime; } //Getter and Setter作ってね }
@Entity
はDiaryクラスがエンティティクラスであることを示すために必要です。この定義がないと、SQLの操作に失敗します。理由はSpring Data JPA
にありますが、割愛します。一度このアノテーションを消して試してみるといいと思います。@GeneratedValue
は主キーが自動採番されるため示す必要があります。strategy = GenerationType.IDENTITY
は採番方法を示します。@Column
を付けないとエンティティクラスのフィールド名とテーブルのフィールド名は同じになります。今回はMySQLのネーミングルール(snake_case)とJavaのネーミングルール(camelCase)が違うため明示的に紐づける必要があります。name = "body_text"
は上記の理由です。nullable = false
はフィールドの値をNull禁止にします。
- データベースのテーブル操作で基本となるCRUD操作するために
Spring Data JPAのListCrudRepository
を継承してリポジトリインターフェースDiaryRepository.java
を作成します。 DiaryRepository.java
package com.example.diary.repositories; import org.springframework.data.repository.ListCrudRepository; import com.example.diary.models.Diary; public interface DiaryRepository extends ListCrudRepository<Diary,Integer> { //何も書かない }
DiaryRepository.java
は、ただ単に、ListCrudRepository
を継承しただけのインターフェースです。ただのインターフェースですが、Spring Data JPA
ではこのインターフェースを実装したクラスを自動で作ってくれるため、プログラマはこのDiaryRepository.java
を使ってDiaryテーブルの簡単なCRUD操作ができます。CrudRepositoryを継承しただけのDiaryRepositoryインターフェースを使用してできるCRUD操作には限界があります。例えば条件でテーブルのフィールドを指定してデータを取得したい場合はDiaryRepositoryインターフェースにメソッドを追加する必要があります。また、@Queryアノテーションで実行したいSQLを直接指定することもできます。
DiaryController.java
package com.example.diary.controllers; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import com.example.diary.repositories.Diary; @Controller @RequestMapping("diary") public class DiaryController { @Autowired DiaryRepository diaryRepository; }
- コントローラクラスには
@Controller
をつけます。今回のWebアプリではフロントエンドはView(HTML)になります。この場合のコントローラクラスには@Controller
を使用します。@RestController
Spring BootでRest API(Web API)サービスを作成する場合はフロントエンドにView(HTML)はありません。その場合のコントローラクラスには@RestController
をつけます。 @RequestMapping("diary")
@Controller
クラスには@RequestMapping
を付けています。@RequestMapping
では、ルーティング情報(URLマッピング)を設定します。ここでは、@RequestMapping
の引数に「"diary"
」を指定指定ます。そのためDiaryController
は以下のURLにリクエストされた時に受け付けます。http://localhost:8080/diary
@Autowired
Springでは、@Autowired
を付けるとプログラマがリポジトリクラスやサービスクラスをnewしなくても、オブジェクトを自動でセット(注入)してくれます。(Dependancy Injection)
-
DiaryController.java
package com.example.diary.controllers; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import com.example.diary.repositories; /* *ここから下のimportを追加 */ import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; import java.util.List; import com.example.diary.models; @Controller @RequestMapping("diary") public class DiaryController { @Autowired DiaryRepository diaryRepository; //日記一覧の取得 @GetMapping("summary") public String summary(Model model) { List<Diary> diaries = diaryRepository.findAll(); //diaryテーブルから日記の一覧情報を取得 model.addAttribute("diaries", diaries); return "summary"; } }
-
@GetMapping("summary")
summary
メソッドには@GetMapping("summary")
をつけています。@GetMapping
は@RequestMapping
と同様にルーティング情報(URLマッピング)を設定しますが、HTTPのGETリクエストだけを受け付け、POSTリクエストは受け付けません。@RequestMapping
は、HTTPリクエストのGET・POST関係なくURLが合っていれば受け付けます。
よってsummary
メソッドは、以下のURLにHTTP GETリクエストされた時に呼び出されます。http://localhost:8080/diary/summary
-
model.addAttribute("diaries", diaries)
Model
クラスのaddAttribute
メソッドを使用することで、サーバーサイドからフロントエンドへデータの受け渡しができるようになります。つまり、ここでは日記一覧データが入っている変数diaries
をフロントエンドへ返しています。こうすることでフロントエンド側でdiaries
という変数で日記一覧データを扱えるようになります。 -
return "summary"
summary.html
ファイルをフロントエンドに返すことを意味しています。
- 日記一覧データを表示するためのフロントエンドの作成ですが、Spring Bootでは純粋なHTMLファイルではなくテンプレートエンジンのThymeleafを使用します。
テンプレートエンジンはHTMLのひな形がベースにあり、そのHTMLのひな形にデータを合成して、最終的に純粋なHTMLファイルを出力する事ができます。
今回開発しようとしている日記一覧データの表示では日記一覧データをHTML内で合成するので、その部分はプログラムのようなコードを書く必要があります。Thymeleafは他にも便利な機能やいろいろな使い方があります。
- 変数のエスケープ
- 条件分岐や繰り返し操作
- 基本オブジェクトや日付・文字列・数値のユーティリティオブジェクト
- HTMLの共通化テンプレート、フラグメント化
-
日記一覧データを表示するためのテンプレートファイルのThymeleafを作成します。
src/main/resources/template
ディレクトリ下にsummary.html
というファイル名で新規作成します。<!DOCTYPE html> <html xlmns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>[Spring Boot] Diary web applications [Thymeleaf]</title> </head> <body> <h1>Diary list</h1> <table> <thead> <tr> <th></th> <th>datetime</th> <th></th> <th></th> </tr> </thead> <tbody> <tr th:each="diary : ${diaries}"> <td th:text="${diary.bodyText}"></td> <td th:text="${diary.createDatetime}"></td> <td> <a th:href="@{/diary/edit(id=${diary.id})}">edit</a> </td> <td> <form th:action="@{/diary/delete}" method="post"> <input type="hidden" name="id" th:value="${diary.id}"/> <input type="submit" value="delete"/> </form> </td> </tr> </tbody> </table> <h3>New post</h3> <form th:action="@{/diary/add}" method="post"> <input type="text" name="bodyText"/> <input type="submit" value="submit"/> </form> </body> </html>
テンプレートファイルのファイル名について
テンプレートファイルのファイル名は、コントローラクラスのメソッドの戻り値に合わせる必要があります。
コントローラクラスDiaryController.java
のsummary
メソッドの戻り値はreturn "summary";
でしたので、テンプレートファイルのファイル名はsummary.html
にする必要があります。 -
Thymeleafのファイルは、htmlタグで下のように
th
の名前空間(ネームスペース)を宣言します。<html xmlns:th="http://www.thymeleaf.org">
この名前空間がないと動作しないわけではないがお約束で書きましょう。
-
<table>
タグの中で日記一覧データの表示をしています。<tr th:each="diary : ${diaries}"> <td th:text="${diary.bodyText}"></td> <td th:text="${diary.createDatetime}"></td> ... </tr>
コントローラクラス
DiaryController.java
のsummary
メソッドで、Model
クラスを使って日記一覧データをdiaries
という変数に入れてフロントエンドに返しました。
ここではそのdiaries
を使っています。diaries
には日記データが複数入っているためth:each
で繰り返し処理をしています
日記データを1件ずつdiary
という変数に入れなおし、th:text="${diary.bodyText}"
で日記の本文を表示し、th:text="${diary.createDatetime}"
で日記の投稿日時を表示しています。summary.html
には書く日記の編集と削除ボタンがありますが、現段階では対応する処理を実装していませんのでボタンを押してもエラーになります。
- 日記一覧データを取得するための処理を書いた
DiaryController.java
に日記削除を行うdelete
メソッドを追加します。package com.example.diary.controllers; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; import com.example.diary.repositories; import com.example.diary.models; import java.util.List; @Controller @RequestMapping("diary") public class DiaryController { @Autowired DiaryRepository diaryRepository; //日記一覧の取得 @GetMapping("summary") public String summary(Model model) { List<Diary> diaries = diaryRepository.findAll(); //diaryテーブルから日記の一覧情報を取得 model.addAttribute("diaries", diaries); return "summary"; } //指定されたidの日記を削除する @PostMapping("delete") public String delete(@RequestParam int id) { diaryRepository.deleteById(id); return "redirect:/diary/summary"; } }
@PostMapping("delete")
delete
メソッドには@PostMapping("delete")
をつけています。@PostMapping
は@RequestMapping
と同様にルーティング情報(URLマッピング)を設定しますが、HTTPのPOSTリクエストだけを受け付け、GETリクエストは受け付けません。@RequestMapping
は、HTTPリクエストのGET・POST関係なくURLが合っていれば受け付けます。
よってdelete
メソッドは、以下のURLにHTTP POSTリクエストされた時に呼び出されます。http://localhost:8080/diary/delete
return "redirect:/diary/summary";
画面のリダイレクトを行っています。このコードは以下のURLにリダイレクトされます。http://localhost:8080/diary/summary
summary.html
の削除ボタン部分を抜粋して確認してみましょう。
<td> <form th:action="@{/diary/delete}" method="post"> <input type="hidden" name="id" th:value="${diary.id}"/> <input type="submit" value="delete"/> </form> </td>
<input type="hidden" name="id" th:value="${diary.id}"/>
で投稿された日記のid情報を紐づけています。
しかし実際にはこのidをテーブル上に表示する必要はないのでtype="hidden"
を指定して非表示にしています。
また、name="id"
と指定することでPOSTする際にパラメータとしてidという名前でリクエストをPOSTします。URLとしては以下のようなURLになります。http://localhost:8080/diary/delete?id=1
DiaryController.java
に日記の作成を行うadd
メソッドを追加します。package com.example.diary.controllers; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; import com.example.diary.repositories; import com.example.diary.models; import java.util.List; @Controller @RequestMapping("diary") public class DiaryController { @Autowired DiaryRepository diaryRepository; //日記一覧の取得 @GetMapping("summary") public String summary(Model model) { List<Diary> diaries = diaryRepository.findAll(); //diaryテーブルから日記の一覧情報を取得 model.addAttribute("diaries", diaries); return "summary"; } //指定されたidの日記を削除する @PostMapping("delete") public String delete(@RequestParam int id) { diaryRepository.deleteById(id); return "redirect:/diary/summary"; } //日記の新規登録 @PostMapping("add") public String add(@RequestParam String bodyText) { Diary diary = new Diary(0, bodyText, LocalDateTime.now().truncatedTo(ChronoUnit.SECONDS)); //ChronoUnitで秒より下を切り捨て diaryRepository.save(diary); return "redirect:/diary/summary"; } }
- マッピングされるURLや、パラメータ名は
delete
と一緒ですね。説明は割愛します。 Diary diary = new Diary(0, bodyText, LocalDateTime.now().truncatedTo(ChronoUnit.SECONDS));
Diary
クラスのコンストラクタにはid
,bodyText
,createDatetime
の情報が必要ですのでこれらを渡してインスタンスを生成します。また、投稿日時にはミリ秒が必要ないので秒より下を切り捨てています。
- Spring Bootでバリデーション機能を開発する場合、バリデーションチェックのルールを作成し、機能を活用していくことになります。
プロジェクト作成時にValidation
にチェックを入れておくことで機能を追加することができます。
また、忘れていた場合でも以下の依存関係を明示してあげれば使えます。<dependancy> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependancy>
- 今回の日記投稿Webアプリのバリデーションルールを考案します。
今回は登録フォームに本文を記入して登録するだけですのでチェックする対象は本文に対してです。
ここでは、3文字以上150文字以内、空文字を禁止としておきます。
Diary.java
を編集してバリデーションルールを設定していきます。package com.example.diary.models; import java.time.LocalDateTime; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; /* * 以下のimportを追加。 */ import javax.validation.constraints.NotNull; import javax.validation.constraints.Size; @Entity public class Diary { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private int id; @NotNull @Size(min = 3, max = 150) @Column(name = "body_text", nullable = false) private String bodyText; @Column(name = "create_datetime", nullable = false) private LocalDateTime createDatetime; public Diary(){} public Diary(int id, String bodyText, LocalDateTime createDatetime) { this.id = id; this.bodyText = bodyText; this.createDatetime = createDatetime; } //Getter and Setter作ってね }
- このバリデーションルールを設定するフィールド名と
Thymeleaf
テンプレートファイルのname
属性を合わせる必要があります。
抜粋して確認してみましょう。<form th:action="@{/diary/add}" method="post"> <input type="text" name="bodyText"/> <input type="submit" value="submit"/> </form>
@NotNull
と@Size(min = 3, max = 150)
これらのアノテーションでNullの禁止と文字列の長さ(最小3文字、最大150文字)を指定しています。エラーメッセージの変更
バリデーションチェックでエラーになった場合、画面側でエラーメッセージが表示することができますが、Spring Bootではエラーメッセージの文言を指定しなくても、デフォルトで適当な文言を表示してくれます。
エラーメッセージの文言を自分で指定する場合、バリデーションチェックのアノテーションにmessage
属性を付けます。@Size(min = 3, max = 150, message = "本文は3文字から150文字の間にする必要がありまっせ!")
DiaryController.java
package com.example.diary.controllers; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; import com.example.diary.repositories; import com.example.diary.models; import java.util.List; /* * 以下のimportを追加。 */ import javax.validation.Valid; import org.springframework.validation.BindingResult; @Controller @RequestMapping("diary") public class DiaryController { @Autowired DiaryRepository diaryRepository; //日記一覧の取得 @GetMapping("summary") public String summary(Model model, Diary diary) { List<Diary> diaries = diaryRepository.findAll(); //diaryテーブルから日記の一覧情報を取得 model.addAttribute("diaries", diaries); return "summary"; } //指定されたidの日記を削除する @PostMapping("delete") public String delete(@RequestParam int id) { diaryRepository.deleteById(id); return "redirect:/diary/summary"; } //日記の新規登録 @PostMapping("add") public String add(Model model, @Valid Diary diary, BindingResult bindingResult) { if(bindingResult.hasErrors()) { return summary(model, diary); } diary.setCreateDatetime(LocalDateTime.now().truncatedTo(ChronoUnit.SECONDS)); diaryRepository.save(diary); return "redirect:/diary/summary"; } }
summary
メソッドの変更点は以下の点です。- public String summary(Model model) { + public String summary(Model model, Diary diary) { List<Diary> diaries = diaryRepository.findAll(); //diaryテーブルから日記の一覧情報を取得 model.addAttribute("diaries", diaries); return "summary"; }
- バリデーションエラー時にエラーメッセージを表示するために、フロントエンド側のHTML(
summary.html
)でdiary
を使うようになったからです。
summary
メソッドの引数にDiary diary
を追加すると、summary
メソッドが呼ばれた時にmodel
にdiary
オブジェクトが自動でセットされ、そのままmodel
がdiary
オブジェクトをフロント側へ返すという流れになるので、フロント側(HTML側)でdiary
オブジェクトが使えるようになります。
- バリデーションエラー時にエラーメッセージを表示するために、フロントエンド側のHTML(
add
メソッドの変更点は以下の点です。- public String add(@RequestParam String bodyText) { + public String add(Model model, @Valid Diary diary, BindingResult bindingResult) { + if(bindingResult.hasErrors()) { + return summary(model, diary); + } - Diary diary = new Diary(0, bodyText, LocalDateTime.now().truncatedTo(ChronoUnit.SECONDS)); //ChronoUnitで秒より下を切り捨て + diary.setCreateDatetime(LocalDateTime.now().truncatedTo(ChronoUnit.SECONDS)); diaryRepository.save(diary); return "redirect:/diary/summary"; }
- 引数に
Diary
を指定する事で、新規登録フォームの本文bodyText
の値Diary
のフィールドbodyText
に入るため、今まで引数に指定していた@RequestParam String bodyText
は必要なくなるので削除しています。
また、引数のDiary
には@Valid
を付けています。@Valid
を付ける事で、フォームクラスDiary
の各フィールドのバリデーションチェック機能が有効になります。
そして、Diary
のバリデーションチェックの結果が、もう一つの引数のBindingResult bindingResult
に入ります。
具体的に言うと、bindingResult
にはエラーが存在したか、エラーが何個あるか、エラー内容などの情報が入ります。一度デバッグをしてbindingResult
の値を確認すると、イメージが掴みやすくなると思います。@Valid、@Validated
上では引数のフォームクラスに@Valid
を付けていますが、@Valid
はjavax.validation.Valid
で、JavaのJakarta Bean Validationです。
また、@Valid
とは別に@Validated
もあります。@Validated
はorg.springframework.validation.annotation.Validated
で、Springの機能です。 - 次に
add
メソッド内の最初のコードですif(bindingResult.hasErrors()) { return summary(model, diary); }
bindingResult
のhasErrors
メソッドでバリデーションエラーが存在するかどうかを判別し、エラーがあった場合は登録処理をせずにsummary
メソッドを呼び出します。summary
メソッドにエラー情報を渡すことでエラー情報をフロントエンドに送ることができます。この時、
add
メソッドやsummary
メソッド内では、model
にdiary
とbindingResult
の値が自動的にセットされています。そのため、フロント側でエラーがあればエラーメッセージを表示するようになります。 - 最後に
add
メソッド内の変更箇所です。引数のdiary.setCreateDatetime(LocalDateTime.now().truncatedTo(ChronoUnit.SECONDS));
@RequestParam String bodyText
を削除したので、投稿された本文のデータは、Diary
クラスのbodyText
から取得しています。
- 引数に
- フロントエンドの日記一覧情報と新規登録フォームがある
summary.html
を変更します。<!DOCTYPE html> <html xlmns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>[Spring Boot] Diary web applications [Thymeleaf]</title> </head> <body> <h1>Diary list</h1> <table> <thead> <tr> <th></th> <th>datetime</th> <th></th> <th></th> </tr> </thead> <tbody> <tr th:each="diary : ${diaries}"> <td th:text="${diary.bodyText}"></td> <td th:text="${diary.createDatetime}"></td> <td> <a th:href="@{/diary/edit(id=${diary.id})}">edit</a> </td> <td> <form th:action="@{/diary/delete}" method="post"> <input type="hidden" name="id" th:value="${diary.id}"/> <input type="submit" value="delete"/> </form> </td> </tr> </tbody> </table> <h3>New post</h3> <form th:action="@{/diary/add}" method="post"> <input type="text" name="bodyText"/> <input type="submit" value="submit"/> + <div th:if="${#fields.hasErrors('diary.bodyText')}" th:errors="*{diary.bodyText}"></div> </form> </body> </html>
summary.html
の変更点は以下の1行です。新規投稿フォームの本文の内容がバリデーションチェックに引っかかった場合、エラーメッセージを表示しています。<div th:if="${#fields.hasErrors('diary.bodyText')}" th:errors="*{diary.bodyText}"></div>
th:if="${#fields.hasErrors('diary.bodyText')}"
で、フォームクラスdiary
のフィールドbodyText
にエラーがあったかどうかを判別し、エラーがあればth:errors="*{diary.bodyText}"
でエラーメッセージを表示しています。
- フロントエンドの編集画面を作成します。編集画面には一覧画面のeditから遷移するようにしています。
その部分のhtmlを抜粋します。<td> <a th:href="@{/diary/edit(id=${diary.id})}">edit</a> </td>
- 編集画面への遷移URLは以下の通りです。
http://localhost:8080/diary/edit?id=2
- 編集画面
edit.html
の作成画面自体は編集するためのテキストボックスと更新ボタンだけなので、とてもシンプルです。<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>[Spring Boot] Diary web applications [Thymeleaf] - edit</title> </head> <body> <h1>Edit diary</h1> <form th:action="@{/diary/update}" method="post"> <input type="text" name="bodyText" th:value="${diary.bodyText}"/> <input type="hidden" name="id" th:value="${diary.id}"/> <input type="submit" value="update"/> <div th:if="${#fields.hasErrors('diary.bodyText')}" th:errors="*{diary.bodyText}"></div> <a href="/diary/summary">Return to list</a> </form> </body> </html>
編集画面のソース(edit.html
)を見ると、画面を開いた時にテキストボックスは編集前の日記本文を表示するためにth:value="{diary.bodyText}"
という属性を付けています。
<input type="hidden"/>
に編集する日記のid
を保持するためにth:value="${diary.id}"
という属性を付けています。
このth:value="${…}"
は、バックエンドのコントローラクラスから受け取る情報ですので、属性の値はこれから作るコントローラクラスと合わせる必要があります。
また、編集する日記本文の内容がバリデーションチェックでエラーになった場合、エラーメッセージを表示するようにしています。
- メソッドを追加した
DiaryController.java
です。package com.example.diary.controllers; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; import com.example.diary.repositories; import com.example.diary.models; import java.util.List; import javax.validation.Valid; import org.springframework.validation.BindingResult; @Controller @RequestMapping("diary") public class DiaryController { @Autowired DiaryRepository diaryRepository; //日記一覧の取得 @GetMapping("summary") public String summary(Model model, Diary diary) { List<Diary> diaries = diaryRepository.findAll(); //diaryテーブルから日記の一覧情報を取得 model.addAttribute("diaries", diaries); return "summary"; } //指定されたidの日記を削除する @PostMapping("delete") public String delete(@RequestParam int id) { diaryRepository.deleteById(id); return "redirect:/diary/summary"; } //日記の新規登録 @PostMapping("add") public String add(Model model, @Valid Diary diary, BindingResult bindingResult) { if(bindingResult.hasErrors()) { return summary(model, diary); } diary.setCreateDatetime(LocalDateTime.now().truncatedTo(ChronoUnit.SECONDS)); diaryRepository.save(diary); return "redirect:/diary/summary"; } //編集画面を表示する @GetMapping("edit") public String edit(Model model, Diary diary) { Diary originalDiary = diaryRepository.findById(diary.getId()).get(); diary.setBodyText(originalDiary.getBodyText()); diary.setCreateDatetime(originalDiary.getCreateDatetime()); model.addAttribute("diary", diary); return "edit"; } //日記を更新する @PostMapping("update") public String update(Model model, @Valid Diary diary, BindingResult bindingResult) { if (bindingResult.hasErrors()) { return edit(model, diary); } diary.setCreateDatetime(LocalDateTime.now().truncatedTo(ChronoUnit.SECONDS)); diaryRepository.save(diary); return "redirect:/diary/summary"; } }
edit
メソッド内の1行目ですが、まずは編集対象の日記をDatabaseのdiaryテーブルから取得します。Diary originalDiary = diaryRepository.findById(diary.getId()).get();
diary
テーブルからの取得は、リポジトリインタフェースDiaryRepository
を使用しています。findById
メソッドは、Spring Data JPAのListCrudRepository
が宣言しているメソッドで、戻り値はOptional<T>
です。Optional<T>
から値の実体を取得するにはget
メソッドを実行する必要があります
ただし、値がない場合に実行すると例外を吐くため本来はisPresent
メソッドでデータがあることを確認してからget
する必要があります。
今回は、必ず取れる前提で直接get
メソッドを実行しています。- 次に、
edit
メソッド内の2行目です。Model
クラスのaddAttribute
メソッドを使用して、編集対象の日記オブジェクトoriginalDiary
をフロントエンドへ返す準備をしています。model.addAttribute("diary", diary);
- 次に、日記の更新処理を行う
update
メソッドについてです。 update
メソッドの引数には@Valid Diary diary
がありますので、diary
のフィールドのid
とbodyText
に、HTTPのPOSTリクエストされるid
とbodyText
を受け取る事ができます。
この変数名のid
とbodyText
は、HTMLフォームのname
属性名と一致させる必要があります。
上で書きました編集画面のedit.html
の一部を抜粋して確認してみます。<form th:action="@{/diary/update}" method="post"> <input type="text" name="bodyText" th:value="${diary.bodyText}"> <input type="hidden" name="id" th:value="${diary.id}"/> ...
text
とhidden
のinput
タグがあり、その2つのデータがbodyText
とid
というname
属性でPOSTリクエストで送信されてくるのがわかります(id
は編集対象の日記id
で、bodyText
は本文です)。
また、引数のdiary
には@Valid
が付いていますので、diary
の各フィールドのバリデーションチェック機能が有効になります。
そして、diary
のバリデーションチェックの結果が、もう一つの引数のBindingResult bindingResult
に入ります。- 最後に渡された
diary
に現在の日時を追加して保存します。(編集時の時間をCreateDatetime
とする)diary.setCreateDatetime(LocalDateTime.now().truncatedTo(ChronoUnit.SECONDS)); diaryRepository.save(diary);
diary
テーブルのデータ更新処理は、リポジトリインタフェースDiaryRepository
を使用しています。save
メソッドは、Spring Data JPAのListCrudRepository
が宣言しているメソッドです。リポジトリクラスの新規作成処理と更新処理のコードは同じ
リポジトリクラスdiaryRepository
の更新処理のコードdiaryRepository.save(diary);
ですが、これは新規投稿時(add
メソッド)と同じです。
save
メソッドの実装ソースコードを見ると、Databaseのテーブルにレコードが存在するかどうかで実行するSQLをinsert
かupdate
で使い分けてくれています。通称upsert
と言います。
- 参考にさせていただいたサイトSpring Boot2で日記投稿ウェブアプリ開発入門