【はじめに】
この記事は【マイスター・ギルド】本物の Advent Calendar 2021の12日目の記事です。
ためになる技術的な記事から、猫ちゃんに癒やされる記事、健康オタクになれる記事まで、バラエティに富んだ記事が投稿されているので、ぜひチェックしてみてください!
【経緯】
こちらの記事は前記事のSpring Bootで簡単なWebアプリを作ってみたい!【準備からGet・Post表示まで】の続きになります。
Javaの入門書である「スッキリわかるJava入門」を読み終わったので、なにかWebアプリを作ってみたいと思ったのがきっかけです。
ちょうど業務でSpring Bootが使われていたので勉強を始めたのですが、チュートリアルが少なく苦労しているので、備忘録的にまとめようと思っています。
この記事だけで、CRUDの実装はできるようになっていますが、前記事で説明した内容は省略しています。
不明点があった場合は前記事も参照していただければと思います。
【対象読者】
Javaの入門書を読み終わって、これからWebアプリを作ってみたいと思っているひと向けです。
説明がわかりにくかったり、間違っている部分があれば、コメントいただければありがたいです。
【この記事でやること】
ToDoアプリを題材にSpring BootでCRUD処理を実装します。
■テストアプリケーションのイメージ
最小限の機能でToDoアプリを作成します。
下記に動作イメージを示します。
【開発環境】
- OS: Windows 10 Pro
- IDE: IntelliJ IDEA 2021.2.2(Community Edition)
- Java: openjdk 11.0.2 2019-01-15
- Spring Boot: 2.6.1
- MySQL: 8.0.26
【準備編】
■MySQLのインストール
下記を参考にMySQLのインストールとユーザー登録、動作確認までします。
サイトに詳しく手順が書いてあるので、ここでは説明を省きます。
ユーザー名とパスワードは後ほど使うので、メモしておきます。
-
MySQLのインストールで参考にしたサイト
Windows: https://prog-8.com/docs/mysql-env-win
Mac: https://prog-8.com/docs/mysql-env -
MySQLのDL元
Windows: https://downloads.mysql.com/archives/installer/
【実装編】
■ToDoアプリを作成して、CRUDしてみよう(MySQL)
- Spring initializrでプロジェクトを作成
今回もSpring initializrでプロジェクトを用意します。
設定を下記のようにして、GENERATEします。
- Dependencies
- Lombok : アノテーションを付けるだけで、GetterやSetterを自動で生成してくれるライブラリ
- Spring Data JPA : 簡単にDBアクセスができるライブラリ
- MySQL Driver : MySQLとの接続のためのドライバー(だと思われる)
GENERATEしたzipを解凍してIDEで起動します。
- Modelの作成
src\main\java
のcom.example.SpringBootTodo
にmodel
パッケージを作成し、Todo.java
を作成します。
package com.example.SpringBootTodo.model;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import lombok.Getter;
import lombok.Setter;
@Getter // フィールドのgetメソッド(id, content)を自動生成する
@Setter // フィールドのsetメソッド(id, content)を自動生成する
@Entity // データの入れ物であるEntityクラスであることを指定する
public class Todo {
@Id // Entityの主キーを設定する
@GeneratedValue // Entityの値を自動採番する
private Long id;
private String content;
}
- モデルは、DBテーブルのカラム名をイメージしながらフィールドとそれぞれのGetter、Setterを用意します
- Lombokを使えば、
@Getter
、@Setter
アノテーションを付けるだけで、自動生成してくれます
- Lombokを使えば、
- Entityクラスであること示すために
@Entity
アノテーションをつけます- Entityクラスでモデルを作成することで、クラス名と同じ名前のテーブルをMySQLに作成してくれます
-
id
フィールドには@Id
、@GenerateValue
アノテーションを付けておくと、主キー設定され、自動採番されるようになります
- Repositoryの作成
com.example.SpringBootTodo
にrepository
パッケージを作成し、TodoRepository.java
を作成します。
package com.example.SpringBootTodo.repository;
import com.example.SpringBootTodo.model.Todo;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository // Repositoryクラスであることを示す
public interface TodoRepository extends JpaRepository<Todo, Long> {
}
- Repositoryインターフェースを作成して
JpaRepository
を継承するだけで、DBのレコード取得や保存などの便利な機能を使えるようになります-
JpaRepository<Entityクラス名, IDの型>
で継承します
-
- 各Entityクラス毎に作成します
- Controllerの作成
com.example.SpringBootTodo
にcontroller
パッケージを作成し、TodoController.java
を作成します。
package com.example.SpringBootTodo.controller;
import com.example.SpringBootTodo.model.Todo;
import com.example.SpringBootTodo.repository.TodoRepository;
import lombok.RequiredArgsConstructor;
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.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
@RequiredArgsConstructor
@Controller
public class TodoController {
private final TodoRepository repository;
// Read
@GetMapping("/")
public String showAllTodo(@ModelAttribute Todo todo, Model model) {
model.addAttribute("todos", repository.findAll());
return "index";
}
// Create
@PostMapping("/add")
public String addTodo(@ModelAttribute Todo todo) {
repository.save(todo);
return "redirect:/";
}
// Update
@PostMapping("/edit")
public String editTodo(@ModelAttribute Todo todo) {
repository.save(todo);
return "redirect:/";
}
// Delete
@GetMapping("/delete/{id}")
public String deleteTodo(@PathVariable Long id) {
repository.deleteById(id);
return "redirect:/";
}
}
- Controllerクラスには、CRUDのそれぞれのメソッドを用意しています
- フィールドには先程作成したRepositoryを用意します
-
@RequiredArgsConstructor
アノテーションをつけることで、下記のようなコンストラクタインジェクションを自動生成してくれます
//省略
@Controller
public class TestController {
private final TodoRepository repository;
public TestController(TodoRepository repository){
this.repository = repository;
}
//省略
}
CRUDそれぞれのメソッドを説明します。
Read
// Read
@GetMapping("/")
public String showAllTodo(@ModelAttribute Todo todo, Model model) {
model.addAttribute("todos", repository.findAll());
return "index";
}
-
@ModelAttribute Todo todo
でJava側の変数todo
とThymeleafの変数${todo}
とを紐付けます- Thymeleafのコードはこの後に説明しているので、合わせて見ていただければと思います
-
repository.findAll()
でDBのtodoテーブルからすべてのデータを取り出します
Create
// Create
@PostMapping("/add")
public String addTodo(@ModelAttribute Todo todo) {
repository.save(todo);
return "redirect:/";
}
- リクエストで受け取った値を
repository.save(todo)
でtodoテーブルに保存します- saveの引数にはEntity(ここではtodo)を指定します
-
return "redirect:/"
は、redirect:
のあとに指定したルートを再度呼び出します- 今回は"/"なので、再度Readの処理が実行されます
Update
// Update
@PostMapping("/edit")
public String editTodo(@ModelAttribute Todo todo) {
repository.save(todo);
return "redirect:/";
}
- Createと全く同じメソッドで実装可能です
- なぜ同じメソッドで異なる処理が可能かというと、リクエストでidが送られてくるかどうかで挙動が異なるからです
- Createの場合はEntity(ここでいうtodo)のメンバ変数idはnullで送られてくるため、バインドした際にidが自動採番されて新規に登録されます
- Updateの場合はEntityのidが指定された状態で送られてくるため、そのidに対応するレコードの修正をするようになっています
- 今回は説明のためにCreateとUpdateで別のメソッドを用意しましたが、Post先を同じにすれば一つのメソッドで実現可能です
Delete
// Delete
@GetMapping("/delete/{id}")
public String deleteTodo(@PathVariable Long id) {
repository.deleteById(id);
return "redirect:/";
}
-
@GetMapping("/delete/{id}")
の{id}
でURLにid値を設定することができます -
@PathVariable Long id
でURLの{id}
の値が変数id
に代入されます -
repository.deleteById(id)
で指定したid値のレコードを削除します- ここはEntityを指定するのではなくidを指定します
- repositoryのメソッドはここで使ったもの以外にもあるので、興味がある方は公式リファレンスを参照ください
- Thymeleafテンプレートの作成
src\main\resources\templates
にindex.html
を作成します。
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>ToDoApp</title>
</head>
<body>
<h2>ToDo新規追加</h2>
<!--Create-->
<form th:action="@{/add}" th:object="${todo}" method="post">
<label for="content">ToDo:</label>
<input type="text" th:field="*{content}">
<button>登録</button>
</form>
<br>
<h2>ToDo一覧</h2>
<div th:if="${todos.size() == 0}">
タスクがありません
</div>
<div th:if="${todos.size() > 0}">
<!--Read & Edit-->
<div th:each="todo : ${todos}"> <!--th:each 繰り返し処理をおこなう-->
<form th:action="@{/edit}" th:object="${todo}" method="post" style="display: inline">
<input type="hidden" name="id" th:value="*{id}">
<label for="content">[[*{id}]] : </label>
<input type="text" name="content" th:value="*{content}">
<button>更新</button>
</form>
<!--Delete-->
<a th:href="@{/delete/{id}(id=${todo.id})}">削除</a>
</div>
</div>
</body>
</html>
-
th:object
でフォームにバインドするオブジェクトを指定します- 今回は、Controllerで指定した
todo
を設定しています
- 今回は、Controllerで指定した
-
th:field
はth:object
で指定したオブジェクトのフィールド名を指定します -
th:if
でif文を作成することができます -
th:each
で繰り返し処理を行うことができます。拡張for文のような記述をしています-
${todos}
はControllerのReadのodel.addAttribute("todos", repository.findAll())
で指定した"todos"
と紐付いています
-
-
[[]]
でモデルの値を表示させることができます -
<a th:href="@{/delete/{id}(id=${todo.id})}">
で{id}
にtodo.id
を値を代入したリンクを作成できます
- DBの作成
MySQLのDBの作成をします。
下記サイトの「1.データベースの作成」を参照して、好きな名前でDBを作成します。
ここではDB名をspring_boot_todo
としました。
- propertiesの修正
src\main\resources
のapplication.properties
を修正します。
# DBの接続先情報
spring.datasource.url=jdbc:mysql://localhost:3306/spring_boot_todo
spring.datasource.username=root
spring.datasource.password=root
spring.jpa.hibernate.ddl-auto=create
-
jdbc:mysql://localhost:3306/spring_boot_todo
の最後の部分は先程作成したDB名を指定します -
spring.datasource.username=
とspring.datasource.password=
はMySQLのインストールで作成したアカウントを指定します -
spring.jpa.hibernate.ddl-auto=create
はspring bootの起動のたびにDBをどうするかを指定します- createは実行の度に既存のテーブルを削除して新たに作成します
- 動作確認
プログラムを実行して、http://localhost:8080
にアクセスすると下のような画面が表示されます。
新規追加のFormにToDoを入力します。
登録ボタンを押すと、ToDo一覧に先程の内容が追加されます。(Create & Read)
一覧のフォームの内容を修正して更新ボタンを押すと、内容が更新されます。(Update)
わかりにくいですが、ブラウザの更新ボタンを押しても更新後の内容が表示されていれば正しく動作しています。
ToDo一覧の削除リンクを押すと一覧から消えます。(Delete)
以上で実装完了です!
【まとめ】
今回はSpring BootでToDoアプリを題材にCRUD処理の実装を試してみました。
次回は今回実装したToDoアプリをRestAPIにしたいと思っています。
(フロント側の学習も必要になるので、いつ投稿できるかわかりませんが、ご期待ください。)