SpringBoot + MyBatis + Thymeleaf + MySQL で超簡潔なCRUD処理

[以前に投稿させていただいた記事](https://qiita.com/hhujk5/items/f0b39b5ad3c76d6d355c)の続き的なものです。
CRUD処理全て実装できたので、自分用のアウトプットとして投稿させていただきます。
記載が稚拙だったり誤っている箇所等あるかもしれませんがご容赦ください。
誤りについてはご指摘いただけますと大変幸いですm(_ _)m。

##環境
Spring Boot 2.4.4
Java 11
OS　 ：　macOS Big Sur
IDE　：　SpringToolSuite４
Maven

##目標
SQLでテーブルusers(カラムはid, name, age)を作成し、以下の４つの処理ができるアプリを作成。
新規ユーザー登録（C)
ユーザー情報表示（R)
ユーザー情報更新（U)
ユーザー情報削除（D)

![CRUD処理.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1189089/c55600c2-f659-56d3-1b2d-99e877eb75ba.gif)


##ディレクトリ構成

```
.
├src/main/java/com/example/demo/
│　　　　　　　　　　　　　　　　　├controller / UController.java
│　　　　　　　　　　　　　　　　　├model / User.java
│　　　　　　　　　　　　　　　　　├repository / UMapper.java
│　　　　　　　　　　　　　　　　　├service / UService.java
│　　　　　　　　　　　　　　　　　└MyBatisPracticeApplication.java
└ src/main/resources/
　　　　　　　　　├mapper / UMapper.xml
　　　　　　　　　├templates / users
　　　　　　　　　│　　　　　　　　　├change.html
　　　　　　　　　│　　　　　　　　　├details.html
　　　　　　　　　│　　　　　　　　　├list.html
　　　　　　　　　│　　　　　　　　　├register.html
　　　　　　　　　│　　　　　　　　　└top.html
　　　　　　　　　├application.properties
　　　　　　　　　├data.sql
　　　　　　　　　└schema.sql
```

##各ソースコード
###application.properties

```
spring.datasource.url=jdbc:mysql://localhost:3306/[任意のDB名を記載してください]?serverTimezone=Asia/Tokyo
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.username=root
spring.datasource.password=[MySQLのパスワードを記載してください]

spring.datasource.sql-script-encoding=UTF-8
spring.datasource.initialization-mode=always

spring.datasource.schema=classpath:schema.sql
spring.datasource.data=classpath:data.sql

#MyBatis
mybatis.mapper-locations=classpath*:/mapper/*.xml

#Log Level
logging.level.com.example=debug
```
MySQLの利用に必要な記載と、アプリ実行時に実行するSQLファイルの記載です。
 #MyBatis…読み込むxmlファイルの場所を記載してください。
 #LogLevel…コンソールにSQLの処理を出力してくれます（無くてもOKです）。

###schema.sql

```sql
CREATE TABLE IF NOT EXISTS users (
	id VARCHAR(50) PRIMARY KEY,
	name VARCHAR(50),
	age INT
	);

```
作成するテーブルの内容を記載します。

###data.sql

```sql
INSERT IGNORE INTO users (id, name, age)
	values
		('1', 'Tom', 30),
		('2', 'Mike', 31);
```
usersテーブルに予めINSERTしておくユーザー情報を記載します。

###list.html

```html
<!DOCTYPE html>
<html
	xmlns:th="http://www.thymeleaf.org">
<head>
	<meta charset="UTF-8"></meta>
	<title>ユーザーList</title>
</head>
<body>
	<table border=1>
	<tr>
		<th>ID</th>
		<th>名前</th>
		<th>年齢</th>
	</tr>
	<tr th:each="u:${users}">
		<td th:text="${u.id}"></td>
		<td th:text="${u.name}"></td>
		<td th:text="${u.age}"></td>
	</tr>
	</table>
</body>
</html>
```
http://localhost:8080/users/list
で全ユーザー情報を表示する画面です。
私が初めに作成したselect全件処理時のファイルを残しているだけですので、無くても大丈夫です。

###top.html

```html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
	<meta charset="UTF-8">
	<title>ユーザー一覧</title>
</head>
<body>
	
	<h1>ユーザー一覧(top.html)</h1>
	
	<div th:if="!${users.size()}">
		<p>登録されているユーザーはいません</p>
	</div>
	
	<a th:href="@{/users/register}"><!-- URL「/users/register」生成 -->
		<button>新しいユーザーを登録</button>
	</a>

	<table th:if="${users.size()}" border=1>
	<!-- DB内のデータがtrue = 0以外の時 -->

		<thead>
			<tr>
				<th>ID(PRIMARY KEY)</th>
				<th>名前</th>
				<th>年齢</th>
				<th></th>
				<th></th>
				<th></th>
			</tr>
		</thead>

		<tbody>
			<tr th:each="users:${users}"　th:object="${users}">
				<td th:text="*{id}"></td>
				<td th:text="*{name}"></td>
				<td th:text="*{age}"></td>
				<td><a th:href="@{/users/details/id={id}(id=*{id})}"><button>詳細</button></a></td>
				<td><a th:href="@{/users/change/id={id}(id=*{id})}"><button>変更</button></a></td>
				<td>
					<form th:method="post" th:action="@{/users/delete/id={id}(id=*{id})}"><button>削除</button></a>
					</form></td>
			</tr>
		</tbody>

	</table>

</body>
```
http://localhost:8080/users
で表示されるトップページです。
全ユーザー情報を表示し、各ボタンの機能は以下の通りです。
[新しいユーザーを登録]ボタン→register.htmlへ遷移
[詳細]ボタン→details.htmlへ遷移
[変更]ボタン→change.htmlへ遷移
[削除]ボタン→当該ユーザー情報を削除



###details.html

```html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
	<meta charset="UTF-8">
	<title>ユーザー詳細</title>
</head>

<body>

	<h2>ユーザー詳細表示(details.html)</h2>
	<div th:object="${users}">
		<table border="1">
			<tr>
				<th>ID</th>
				<th>名前</th>
				<th>年齢</th>
			</tr>
			<tr>
				<td th:text="*{id}"></td>
				<td th:text="*{name}"></td>
				<td th:text="*{age}"></td>
			</tr>
	
</body>
</html>
```

###register.html

```html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
	<meta charset="UTF-8">
	<title>新規ユーザー登録</title>
</head>

<body>
	
	<h2>新規ユーザー登録(register.html)</h2>

	<form th:method="post" th:object="${users}" th:action="@{/users/register}">
		<label>ID(VARCHAR):<input type="text" th:field="*{id}"></label><br>
		<label>名前(VARCHAR):<input type="text" th:field="*{name}"></label><br>
		<label>年齢(INT):<input type="text" th:field="*{age}"></label><br>

		<button>新規作成</button>
	</form>
</body>
```
当初はth:fieldの部分を単なるname属性で記載していてそれでも動いたのですが、th:fieldで動かして見たくて書き換えました。
その時以下のエラーが出て悩んでおりました。

>java.lang.IllegalStateException: Neither BindingResult nor plain target object for bean name ‘users’ available as request attribute

その後th:objectについて調べ、ControllerクラスにModelの記載を追加して解決しました。
（参考にさせていただいたteratailの質問：https://teratail.com/questions/26218）


###change.html

```html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
	<meta charset="UTF-8">
	<title>ユーザー変更</title>
</head>

<body>

	<h2>ユーザー変更(change.html)</h2>
	
	<form th:method="post" th:object="${users}" th:action="@{/users/change/id={id}(id=*{id})}">
		<label>ID(VARCHAR):<input type="text" th:field="*{id}"></label><br>
		<label>名前(VARCHAR):<input type="text" th:field="*{name}"></label><br>
		<label>年齢(INT):<input type="text" th:field="*{age}"></label><br>
		<button>変更</button>
	</form>

</body>
</html>
```

###User.java

```java
package com.example.demo.model;

import lombok.Data;

@Data
public class User {
	private String id;
	private String name;
	private int age;
}

```
エンティティクラスです。

###UMapper.xml

```xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

  <mapper namespace="com.example.demo.repository.UMapper">
  
  <resultMap type="com.example.demo.model.User" id="user"><!--  id属性は任意の値 -->
  	<!--  columnはSQLのカラム　propertyはエンティティクラスのフィールド -->
  	<id column="id" property="id"/>
  	<result column="name" property="name"/>
  	<result column="age" property="age"/>
  </resultMap>
 
  <select id="findOne" resultMap="user">
  	select	*
  	from	users
  	where	id = #{id}
  </select>
  
  <select id="find" resultType="com.example.demo.model.User">
  	select	*
  	from	users
  </select>
  
  <insert id="insertOne">
  	insert into users (
  		id,
  		name,
  		age
  	)
  	values (
  		#{id},
  		#{name},
  		#{age}
  	)
  </insert>
  
  <update id="updateOne">
  	update	users
  	set		name = #{name},
  			age = #{age}
  	where	id = #{id}
  </update>
  
  <delete id="deleteOne">
	  	delete from
  		users
  	where
  		id = #{id}  
  </delete>

  </mapper>
```

###UMapper.java

```java
package com.example.demo.repository;

import java.util.List;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;

import com.example.demo.model.User;


@Mapper
public interface UMapper {
	
	//select１件
	public User findOne(String id);
	
	//select全件
	public List<User> find();
	
	//insert
	public void insertOne(User u);
	
	//update
	public void updateOne(@Param("id") String id, @Param("name") String name, @Param("age") int age);
	
	//delete
	public void deleteOne(User u);
	
}
```

###UService.java

```java
package com.example.demo.service;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import com.example.demo.model.User;
import com.example.demo.repository.UMapper;


@Service
public class UService {
	
	@Autowired
	UMapper mapper;
	
	//select１件
	public User getUserOne(String id) {
		return mapper.findOne(id);
	}
	//select全件
	public List<User> getList() {
		return mapper.find();
	}
	
	//insert
	public void insertOne(User u) {
		mapper.insertOne(u);
	}
	
	//update
	public void updateOne(String id, String name, int age) {
		mapper.updateOne(id, name, age);
	}
	
	//delete
	public void deleteOne(User u) {
		mapper.deleteOne(u);
	}
	
}
```

###UController.java

```java
package com.example.demo.controller;

import org.springframework.beans.factory.annotation.Autowired;
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.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.RequestMapping;

import com.example.demo.model.User;
import com.example.demo.service.UService;

import java.util.List;


@Controller
@RequestMapping("/users")
public class UController {
	
	@Autowired
	private UService service;
	
	//select全件表示
	@GetMapping("/list")
	public String getUserList(Model model) {
		List<User> userList =  service.getList();
		model.addAttribute("users", userList);
		return "users/list";
	}	
	

	//トップページ top.html表示
	@GetMapping("")
	public String top(Model model, @ModelAttribute User u) {
		model.addAttribute("users", service.getList()) ;
		return "users/top";
	}
	
	
	//top→[詳細]押下　select１件
	@GetMapping("details/id={id}")
	public String details(@PathVariable("id") String id, Model model) {
		model.addAttribute("users", service.getUserOne(id));
		return "users/details";
	}
	
	
	//top→[新規作成]押下 th:hrefにより生成されたURLをGETで表示
	@GetMapping("/register")
	public String registerUser(Model model, @ModelAttribute User u) {
		model.addAttribute("users", u);
		return "users/register";
	}
	//register.html内の <form method="post"> で↓へ飛ぶ
	@PostMapping("/register")
	public String create(@Validated @ModelAttribute User u, BindingResult result) {
		if (result.hasErrors()) {
			return "users/register";
		}
		service.insertOne(u);
		return "redirect:/users";
	}
	
	
	//top→[変更]押下時にchange.htmlを表示するGET
	@GetMapping("change/id={id}")
	public String change(@PathVariable("id") String id, Model model) {
		model.addAttribute("users", service.getUserOne(id));
		return "users/change";
	}
	@PostMapping("change/id={id}")
	public String update(@ModelAttribute User u, Model model) {
		service.updateOne(u.getId(), u.getName(), u.getAge());
		return "redirect:/users";
	}
	

	//top→[削除]押下時
	@PostMapping("delete/id={id}")
	public String delete(@PathVariable String id, @ModelAttribute User u) {
		service.deleteOne(u);
		return "redirect:/users";
	}
	
}
```

あとはブラウザに（　http://www.localhost:8080/hello/users　）
と入力すればtopページが表示されるはずです。

お疲れ様でした！

##参考にさせていただいたQiita記事
前回から引き続き、下記記事を参考にさせていただきました。
ありがとうございました！
https://qiita.com/sumichan/items/bdc2a0e909416ae55162