Spring Boot + Thymeleaf+JPA+PostgreSQLでCRUDサンプルの改良・開発(Spring Boot ver 2.2以降)

記事を作成するにあたり@ozaki25さんの以下のサンプルを参考にさせていただきました。
【参考記事のタイトル】
SpringBoot + JPA + Thymeleafで簡単なCRUDを作る②～画面と機能作成まで～
URL:https://qiita.com/ozaki25/items/3b348874b6db5ab4f04f

#開発環境
Windows10
Eclipse
PostgreSQL

#各種フレームワーク
Spring Boot バージョン：2.5.0
Bootstrap バージョン：5.1.0

#Spring Boot ver2.2以降での開発の際に変更になった点
HttpHiddenMethodFilterがデフォルトで無効化(disabled)されました。
それによってHttpメソッドの中のPutメソッド(リソースの更新、作成を担う処理)とDeleteメソッド(リソースの削除を担う処理)の設定がデフォルトの状態のままでは実行できません。
なので、設定ファイル(application.propertiesまたはapplication.yml)の中でHttpHiddenMethodFilterを有効化する必要があります(下記を参照)。

```application.properties
#Hibernateの設定
spring.jpa.hibernate.ddl-auto=update

#PostgreSQLの設定
spring.datasource.driver-class-name=org.postgresql.Driver
spring.datasource.url=jdbc:postgresql://localhost:5432/postgres
spring.datasource.username=[ユーザ名]
spring.datasource.password=[パスワード]

#【Spring Boot 2.2以降での変更点】
#  Http PutまたはDeleteメソッドを使う場合は
#「spring.mvc.hiddenmethod.filter.enabled=true」を追加・設定すること。
spring.mvc.hiddenmethod.filter.enabled=true 
```

#プロジェクトのディレクトリ構成
ディレクトリは以下の構成です(画像が縦に長いので分割して表示させています)。
![画像1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1169664/bc61977e-599c-ec1f-bdcc-c4679118b16d.png)
![画像2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1169664/250a2b68-fbdf-3edc-00a0-f0b7a0e55a65.png)

#pom.xmlファイルの構成
pom.xmlファイルの中身は以下の通りです。

```pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.5.0</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>
	<groupId>com.example</groupId>
	<artifactId>baseball</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>baseball</name>
	<description>Demo project for Spring Boot</description>
	<properties>
		<java.version>11</java.version>
	</properties>
	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-data-jdbc</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-data-jpa</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-thymeleaf</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-validation</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-devtools</artifactId>
			<scope>runtime</scope>
			<optional>true</optional>
		</dependency>
		<dependency>
			<groupId>org.postgresql</groupId>
			<artifactId>postgresql</artifactId>
			<scope>runtime</scope>
		</dependency>
		<dependency>
			<groupId>org.projectlombok</groupId>
			<artifactId>lombok</artifactId>
			<optional>true</optional>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
		<!-- https://mvnrepository.com/artifact/org.webjars/bootstrap 20210829追加-->
		<dependency>
		    <groupId>org.webjars</groupId>
		    <artifactId>bootstrap</artifactId>
		    <version>5.1.0</version>
		</dependency>
		<!-- https://mvnrepository.com/artifact/org.webjars/jquery 20210829追加-->
		<dependency>
		    <groupId>org.webjars</groupId>
		    <artifactId>jquery</artifactId>
		    <version>3.6.0</version>
		</dependency>
		<!-- https://mvnrepository.com/artifact/org.webjars/popper.js 20210829追加-->
		<dependency>
		    <groupId>org.webjars</groupId>
		    <artifactId>popper.js</artifactId>
		    <version>2.9.3</version>
		</dependency>		
	</dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
				<configuration>
					<excludes>
						<exclude>
							<groupId>org.projectlombok</groupId>
							<artifactId>lombok</artifactId>
						</exclude>
					</excludes>
				</configuration>
			</plugin>
		</plugins>
	</build>

</project>

```

#PostgreSQLのテーブル構成
```player.sql
CREATE TABLE public.player
(
    id bigint NOT NULL,
    age integer,
    name character varying(255) COLLATE pg_catalog."default",
    "position" character varying(255) COLLATE pg_catalog."default",
    team character varying(255) COLLATE pg_catalog."default",
    CONSTRAINT player_pkey PRIMARY KEY (id)
)
```

#サーバ側(src/main/java/com.example/baseball)のファイルの中身
次にサーバ側のファイルの中身を見ていきます。
##Controllerクラス
```controller/PlayerController.java
package com.example.baseball.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.DeleteMapping;
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;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestMapping;

import com.example.baseball.domain.Player;
import com.example.baseball.service.PlayerService;

@Controller
@RequestMapping("/players")
public class PlayerController {
	@Autowired
	private PlayerService playerService;
	
	@GetMapping
	public String index(Model model) {
		List<Player> players = playerService.findAll();
		model.addAttribute("players", players);
		return "players/index";
	}
	
	@GetMapping("new")
	public String newPlayer(Model model) {
		return "players/new";
	}
	
	@GetMapping("{id}/edit")
	public String edit (@PathVariable Long id, Model model) {
		Player player = playerService.findById(id);
		model.addAttribute("player",player);
		return "players/edit";
	}
	
	@GetMapping("{id}")
	public String show(@PathVariable Long id, Model model) {
		Player player = playerService.findById(id);
		model.addAttribute("player", player);
		return "players/show";
	}
	
	@PutMapping("{id}") 
	public String update(@PathVariable Long id, @ModelAttribute Player player) {
		player.setId(id);
		playerService.save(player);
		return "redirect:/players";
	}
	
	@DeleteMapping("{id}")
	public String destroy(@PathVariable Long id) {
		playerService.delete(id);
		return "redirect:/players";
	}
	
	//new.htmlの「作成」ボタンをクリックしたときに実行される処理
    // http://localhost:8080/players/new/
	@PostMapping(path="/new")   
	public String getInfo(@ModelAttribute Player player) {
		playerService.insertSave(player);
		return "redirect:/players";
	}
}


```
##Entityクラス
```domain/Player.java
package com.example.baseball.domain;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

import lombok.Data;

@Entity
@Data
public class Player {
	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private Long id;
	private String name;
	private Integer age;
	private String team;
	private String position;
	
	public Long getId() {
		return id;
	}
	
	public void setId(Long id) {
		this.id = id;
	}
	
	public String getName() {
		return name;
	}
	
	public void setName(String name) {
		this.name = name;
	}
	
	public Integer getAge() {
		return age;
	}
	
	public void setAge(Integer age) {
		this.age = age;
	}
	
	public String getTeam() {
		return team;
	}
	
	public void setTeam(String team) {
		this.team = team;
	}
	
	public String getPosition() {
		return position;
	}
	
	public void setPosition(String position) {
		this.position = position;
	}
	
	@Override
	public String toString() {
		return "Player [ id=" + id + ", name=" + name + ", age=" + age + ", team=" + team + ", position=" + position + "]";
	}
}

```
##Repositoryクラス
```repository/PlayerRepository.java
package com.example.baseball.repository;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import com.example.baseball.domain.Player;

@Repository
public interface PlayerRepository extends JpaRepository <Player, Long>  {

}

```

##Serviceクラス
###【Spring Boot 2.2以降の変更点】
データベースから1件のレコードを取得するfindOneメソッドを書くと、findByIdメソッドに変更するよう、推奨されます。
なのでfindByIdメソッドを書くようにしましょう。


```service/PlayerService.java
package com.example.baseball.service;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate; //追加 20210902
import org.springframework.stereotype.Service;

import com.example.baseball.domain.Player;
import com.example.baseball.repository.PlayerRepository;

@Service
public class PlayerService {
	
	@Autowired
	private PlayerRepository playerRepository;
	
	//JDBC Templateを定義する。
	@Autowired
	private final JdbcTemplate jdbcTemplate;
	
	//コンストラクタを定義する。
	public PlayerService(JdbcTemplate jdbcTemplate) {
		this.jdbcTemplate = jdbcTemplate;
	}
	
	public List<Player> findAll(){
		return playerRepository.findAll();
	}
	
    //IDを引数にして1件のレコードを取得する処理
	public Player findById(Long id) {
        return playerRepository.findById(id).get();
    }
	
	
	//追加　20210902
	public Player insertSave(Player player) {
		
		//関数findMaxIdメソッドからIDの最大値を取得して変数maxIdに入れる。
		long maxId = findMaxId();
		//maxIdに1を加算して新規IDを取得する。
		maxId = maxId + 1;
		//クエリ文を実行する。
		jdbcTemplate.update("INSERT INTO PLAYER (id,age,name,position,team) VALUES(?,?,?,?,?) ",
				maxId,player.getAge(),player.getName(),player.getPosition(),player.getTeam());
		
		return player;
	}
	
	/*
	 * IDの最大値を取得する処理
	 * @return IDの最大値
	 */
	public long findMaxId() {
		
		//クエリ文を用意する。
		String queryString = "SELECT MAX(id) FROM PLAYER";
		
		//jdbcTemplate.queryForObjectメソッドから返却された値を入れる変数selectedMaxIdを用意する。
		Object selectedMaxId ="";
		
		//IDの最大値を入れる変数maxIdを用意して0を入れて初期化する。
		long maxId =0;
		
		/*
		 * jdbcTemplate.queryForObjectにクエリ文を入れて実行する。
		 * @selectedMaxId データベースにレコードが存在する場合、IDの最大値が変数selectedMaxIdに格納される。
		 * @selectedMaxId データベースにレコードが存在しない場合、NULLが変数selectedMaxIdに格納される。
		 * */
		selectedMaxId = jdbcTemplate.queryForObject(queryString, long.class);
		
		//変数selectedMaxIdに格納されている値がNULLの場合、変数maxIdを返却(return)する。
		if (selectedMaxId == null) {
			return maxId;
		}
		
		//queryForObjectメソッドでIDの最大値を取得・返却する。
		return jdbcTemplate.queryForObject(queryString, long.class);
	}
	
	/*
	 * 該当レコードを削除する処理
	 * @param id
	 * */
	public void delete(Long id){
		playerRepository.deleteById(id);
	}
}

```

#フロント側(src/main/resources/templates)のファイルの中身
次に画面に表示される(フロント側)側のファイルの中身を見てみましょう。
##index.html
```players/index.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
  <head>
    <meta charset="utf-8" />
    <title>Listing Players - baseball</title>
    <!-- 共通のフレームワーク -->
    <link rel="stylesheet" th:href="@{/webjars/bootstrap/5.1.0/css/bootstrap.min.css}" />
    <script th:src="@{/webjars/jquery/3.6.0/jquery.min.js}"></script>
    <script th:src="@{/webjars/bootstrap/5.1.0/js/bootstrap.bundle.min.js}"></script>
    <script th:src="@{/webjars/bootstrap/5.1.0/js/bootstrap.min.js}"></script>
    <script th:src="@{/webjars/popper.js/2.9.3/umd/popper.min.js}"></script>
  </head>
  <body>
    <div class="container">
      <h1>Listing Players</h1>
      <table class="table">
        <thead>
          <tr>
            <th>ID</th>
            <th>名前</th>
            <th>年齢</th>
            <th>チーム名</th>
            <th>守備位置</th>
            <th></th>
            <th></th>
            <th></th>
          </tr>
        </thead>
        <tbody>
          <!-- ② -->
          <tr th:each="player:${players}" th:object="${player}">
            <!-- ③ -->
            <td th:text="*{id}"></td>
            <td th:text="*{name}"></td>
            <td th:text="*{age}"></td>
            <td th:text="*{team}"></td>
            <td th:text="*{position}"></td>
            <!-- ④ -->
            <td><a class="btn btn-default btn-xs" th:href="@{/players/{id}(id=*{id})}">参照</a></td>
            <td><a class="btn btn-default btn-xs" th:href="@{/players/{id}/edit(id=*{id})}">編集</a></td>
            <td>
              <!-- ⑤ -->
              <form th:action="@{/players/{id}(id=*{id})}" th:method="delete">
                <input class="btn btn-default btn-xs" type="submit" value="削除" />
              </form>
            </td>
          </tr>
        </tbody>
      </table>
      <a class="btn btn-default" href="/players/new">新規作成</a>
    </div>
  </body>
</html>
```

##new.html
```players/new.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
  <head>
    <meta charset="utf-8" />
    <title>New Player - baseball</title>
    <!-- 共通のフレームワーク -->
    <link rel="stylesheet" th:href="@{/webjars/bootstrap/5.1.0/css/bootstrap.min.css}" />
    <script th:src="@{/webjars/jquery/3.6.0/jquery.min.js}"></script>
    <script th:src="@{/webjars/bootstrap/5.1.0/js/bootstrap.bundle.min.js}"></script>
    <script th:src="@{/webjars/bootstrap/5.1.0/js/bootstrap.min.js}"></script>
    <script th:src="@{/webjars/popper.js/2.9.3/umd/popper.min.js}"></script>
  </head>
  <body>
    <div class="container">
      <h1>New Player</h1>
      <!-- 成功例： @{/players} Controllerは@PostMappingと書く-->
      <!-- 成功例：@{/players/new}　Controllerは@RequestMapping(value="/new",method = RequestMethod.POST)と書く -->
      <form th:action="@{/players/new}" th:method="post">
        <div class="form-group">
          <label class="control-label">名前</label>
          <input class="form-control" type="text" name="name" />
        </div>
        <div class="form-group">
          <label class="control-label">年齢</label>
          <input class="form-control" type="number" name="age" />
        </div>
        <div class="form-group">
          <label class="control-label">チーム名</label>
          <input class="form-control" type="text" name="team" />
        </div>
        <div class="form-group">
          <label class="control-label">守備位置</label>
          <input class="form-control" type="text" name="position" />
        </div>
        <button class="btn btn-default" type="submit">作成</button>
      </form>
      <div class="pull-right">
        <a class="btn btn-link" href="/players">一覧画面へ</a>
      </div>
    </div>
  </body>
</html>
```

##edit.html
```players/edit.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
  <head>
    <meta charset="utf-8" />
    <title>Editing Player - baseball</title>
    <!-- 共通のフレームワーク -->
    <link rel="stylesheet" th:href="@{/webjars/bootstrap/5.1.0/css/bootstrap.min.css}" />
    <script th:src="@{/webjars/jquery/3.6.0/jquery.min.js}"></script>
    <script th:src="@{/webjars/bootstrap/5.1.0/js/bootstrap.bundle.min.js}"></script>
    <script th:src="@{/webjars/bootstrap/5.1.0/js/bootstrap.min.js}"></script>
    <script th:src="@{/webjars/popper.js/2.9.3/umd/popper.min.js}"></script>
  </head>
  <body>
    <div class="container">
      <h1>Editing Player</h1>
      <!-- フォームの送信をPostからPutに変更 2021/08/29 -->
      <form th:action="@{/players/{id}(id=*{id})}" th:method="put" th:object="${player}">
        <div class="form-group">
          <label class="control-label">名前</label>
          <!-- ① -->
          <input class="form-control" type="text" th:field="*{name}" />
        </div>
        <div class="form-group">
          <label class="control-label">年齢</label>
          <input class="form-control" type="number" th:field="*{age}" />
        </div>
        <div class="form-group">
          <label class="control-label">チーム名</label>
          <input class="form-control" type="text" th:field="*{team}" />
        </div>
        <div class="form-group">
          <label class="control-label">守備位置</label>
          <input class="form-control" type="text" th:field="*{position}" />
        </div>
        <button class="btn btn-danger" type="submit">更新</button>
      </form>
      <div class="pull-right">
        <a class="btn btn-link" href="/players">一覧画面へ</a>
      </div>
    </div>
  </body>
</html>
```

##show.html
```players/show.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
  <head>
    <meta charset="utf-8" />
    <title>Show Player - baseball</title>
    <!-- 共通のフレームワーク -->
    <link rel="stylesheet" th:href="@{/webjars/bootstrap/5.1.0/css/bootstrap.min.css}" />
    <script th:src="@{/webjars/jquery/3.6.0/jquery.min.js}"></script>
    <script th:src="@{/webjars/bootstrap/5.1.0/js/bootstrap.bundle.min.js}"></script>
    <script th:src="@{/webjars/bootstrap/5.1.0/js/bootstrap.min.js}"></script>
    <script th:src="@{/webjars/popper.js/2.9.3/umd/popper.min.js}"></script>
  </head>
  <body>
    <div class="container">
      <h1>Show Player</h1>
      <div th:object="${player}" class="border border-dark">
        <div>
          <label>ID</label>
          <p th:text="*{id}"></p>
        </div>
        <div>
          <label>名前</label>
          <p th:text="*{name}"></p>
        </div>
        <div>
          <label>年齢</label>
          <p th:text="*{age}"></p>
        </div>
        <div>
          <label>チーム名</label>
          <p th:text="*{team}"></p>
        </div>
        <div>
          <label>守備位置</label>
          <p th:text="*{position}"></p>
        </div>
      </div>
      <div>
        <a class="btn btn-secondary" th:href="@{/players/{id}/edit(id=*{id})}">編集</a>
      </div>
      <div class="pull-right">
        <a class="btn btn-link" href="/players">一覧画面へ</a>
      </div>
    </div>
  </body>
</html>
```

#ローカルサーバーでアプリケーションを起動する。
ここまで出来たらアプリケーションを起動させます。

プロジェクトの上で右クリックして[実行]⇒[Spring Boot]を選択します。
コンソール画面にビルド処理が開始されます。
![画像5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1169664/19374ee9-e4be-9555-1e2f-2c30d76a3b46.png)


ブラウザを立ち上げてURLに以下のアドレスを入力してアプリを表示します。

URL:http://localhost:8080/players

##一覧画面
![画像6.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1169664/1b7af760-0a4b-35a3-0257-3c33862d62a9.png)

##新規作成画面
![画像7.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1169664/2358ce23-29f1-fc88-f88f-7264821d937a.png)

###データを入力して登録する。
![画像8.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1169664/dfd5e113-670e-5954-6a62-0ec0944c1c2f.png)

###再度、一覧表示画面に戻ると登録データが表示されます。
![画像9.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1169664/62beed4b-d4eb-0b80-d4e7-a0dedcb88354.png)

###登録データの中身を閲覧する。
一覧画面にある参照ボタンをクリックします。デザインは凝っていないので悪しからず...
![画像10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1169664/a9d3b815-8e5e-aa59-f891-0ba46cd2f216.png)

##データの更新
一覧画面の「編集」ボタンをクリックして編集画面に遷移します。
![画像11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1169664/51afe07a-3a39-1f00-5ae4-f9c4a392f9dd.png)

###更新すると一覧画面に更新内容が反映されます。
![画像12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1169664/26d92cb2-02af-7eea-504a-8968c4c71a96.png)

##登録データの削除
一覧画面の削除ボタンをクリックすると該当レコードが削除されます。
![画像13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1169664/4bd2922a-a032-6c88-8e12-b7944f7c3260.png)

以上、Spring Boot 2.2以降のバージョンアップによってHttp通信の処理の変更について解説してみました。