【Spring Boot】Validation(入力チェック)+DB保存までをうまく

## はじめに
#### 環境
・`Eclipse(2024)`
・`Spring Boot 3.3.0 (Maven)`
・`MariaDB`
※DBとの連携は済んでいる前提とします。

#### 作成する画面は3種類
↓1.メインとなる入力画面
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3830719/99c8e86b-1459-e80c-841a-7e0df0c903e6.png)
↓2.入力確認画面
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3830719/f362927b-ce86-8609-0343-575118973285.png)
↓3.完了画面(一応)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3830719/3ff23d18-778d-564c-23ac-5510655bb566.png)

#### 今回作成するクラスやHTML,CSSは以下の通り(pom.xmlも追加記述あり)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3830719/dd0b9529-a14a-1fbd-2e17-f4d6ea806bd9.png)


## 概要
・この記事の内容
  ①バリデーションの使い方(コード)
  ②そのバリデーションをうまく使い、入力内容をDBに保存する(新規登録などの際)
  
・バリデーションについてはとても詳しくは書いていませんが、重要な点は書いています。

・今回登録する内容をDBに保存するまでセッションに保存します。
【メリット】
　1.登録内容確認画面から【戻る】ボタンを押下した際に、先ほど入力された値がすでに入力されている。
　2.もし登録内容確認画面まで行き、あとでやろうと思った際に次開くと先ほどの入力内容がすでに入力されている形になる。(セッション保持の時間内とする)
　3.入力された値の受け渡しが楽。

## 依存関係の追加
`Maven` (Validationのみでなく、今回追加した依存関係も一緒に記載します。)
```xml:pom.xml
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
</dependency>

<dependency>
    <groupId>org.mariadb.jdbc</groupId>
    <artifactId>mariadb-java-client</artifactId>
</dependency>

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-validation</artifactId>
</dependency>
```

## 1.Entityの作成
今回は`User.java`というEntityを作成します。
↓DBは以下の通りに作成されている前提とします。
|カラム名 |データ型|備考|
|---|---|---|
|id|INT|AUTO_INCRIMENTのID|
|login_id|VARCHAR(50)|ログインID|
|name|VARCHAR(50)|名前|
|length|VARCHAR(10)|長さ|
|email|VARCHAR(50)|メールアドレス|

※【長さ】は今回のバリデーションのために作成したカラムです...

```java:User.java
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.Data;

@Entity
@Data
@Table(name = "user")
public class User {
	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private Integer id;
	private String loginId;
	private String name;
	private String length;
	private String email;
}
```
#### POINT
・特にこれといったポイントはありませんが、強いて言うと`@Data`というアノテーションを記載することで、`getter/setter`を書く必要がなくなります。

## 2.Repositoryの作成
Repositoryは以下の通りです。
```java:UserRepository.java
import org.springframework.data.jpa.repository.JpaRepository;

import com.example.web.model.User;

public interface UserRepository extends JpaRepository<User, Integer> {

}
```

## 3.Validationの作成(Form)
先にコードを記載します。
```java:UserForm.java
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Size;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class UserForm {

	@NotBlank
	@Pattern(regexp = "[a-zA-Z0-9]*", message = "半角英数のみ有効です。") //英数字であること
	private String loginId;
	
	@NotBlank(message = "入力必須です。")
	private String name;
	
	@NotBlank(message = "入力必須です。")
	@Size(min=4, max=10, message = "4文字以上10文字以内で入力してください。")
	private String length;
	
	@NotBlank(message = "入力必須です。")
	@Email
	private String email;
	
	//セッションの中身をクリア
	public void userFormReset() {
		this.loginId = "";
		this.name = "";
		this.length = "";
		this.email = "";
	}
}
```
#### POINT
・今回使用しているValidationのアノテーションについて解説します。
　`@NotBlank`：文字列がnull又は空白スペース(半角スペースやタブなど)でないかの検証。
　`@Pattern`：指定された正規表現一致するかを検証。
　(今回は半角英数のみの許可にしているため`regexp = "[a-zA-Z0-9]*`としている。)
　`@Size`：文字列やCollectionが指定した範囲の大きさであることを検証。
　(今回は4文字以上10文字以内：`min=4, max=10`)
　`@Email`：文字列がEmail形式か検証。
　※`message`：表示する警告内容をカスタマイズ。それぞれデフォルトで何かしらメッセージは入っている。

 ・"セッションの中身をクリア"に関しては後ほど記載

 ## 4.Serviceの作成
 今回Serviceで行う内容としては、入力された内容をDBに保存するということ。
 次に記載するコントローラと行き来しながら閲覧するのをおすすめします。
 ```java:UserService.java
 import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.SessionAttributes;

import com.example.web.form.UserForm;
import com.example.web.model.User;
import com.example.web.repository.UserRepository;

@Service
@SessionAttributes(types = UserForm.class)
public class UserService {

	@Autowired
	private UserRepository userRepository;
	
	@ModelAttribute(value = "userForm")
	public UserForm userForm() {
		UserForm userForm = new UserForm();
		return userForm;
	}
	
	//入力されたものをDBに保存
	public void insertUser(UserForm userForm) {
		User user = new User();
		
		//Userクラスに詰め替えをする
		user.setLoginId(userForm.getLoginId());
		user.setName(userForm.getName());
		user.setLength(userForm.getLength());
		user.setEmail(userForm.getEmail());
		
		//DBに保存
		userRepository.save(user);
	}
}
```
#### POINT
・詳しいことはつぎのコントローラのところで一緒に解説を行います。
・ここでのポイントは`UserForm`セッションの中身を`User`に詰め替えを行い、DBに保存することです。

## 5.Controllerの作成
先にコードを記載します。
```java:AllController.java
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
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.PostMapping;
import org.springframework.web.bind.annotation.SessionAttributes;

import com.example.web.form.UserForm;
import com.example.web.service.UserService;

@Controller
@SessionAttributes(types = UserForm.class)//////////////////[1]
public class AllController {

	@Autowired
	private UserService userService;

     ////////////////////[1]
	@ModelAttribute(value = "userForm")
	public UserForm userForm() {
		UserForm userForm = new UserForm();
		return userForm;
	}
	
	//メインの画面(登録画面)の表示
	@GetMapping("/")
	public String showHome(UserForm userForm) { //UserFormの入力を忘れないように
		return "index";
	}
	
	//確認ボタンを押下されたときの処理
	@PostMapping("/confirmation")
	public String check(
			@Validated UserForm userForm, ////////////////[2]
			BindingResult bindingResult) {
		
		//バリデーションチェック
		if(!bindingResult.hasErrors()) {  ////////////////[3]
			//すべて正しく入力されていた場合
			return "user_confirm";
		}else {
			//一つでも入力不足だった場合
			return showHome(userForm);
		}
	}
	
	//登録の確定ボタンが押下されたときの処理
	@PostMapping("/register")
	public String register(UserForm userForm) {
		
		//登録処理をUserService側に任せる
		userService.insertUser(userForm); ////////////////////[4]
		
		//登録が終わったらセッションの中身をクリアする
		userForm.userFormReset();  /////////////////////[5]
		
		return "complete";
	}
 
    //戻る系のボタンが押下された場合
	@GetMapping("/back")
	public String backHome() {
		return "redirect:/";
	}
}
```
#### POINT
・コード内に書かれている番号ごとに解説を行います。

　[1]：これを書くことで、UserFormのセッションを作成することができます。
正直、自分はお決まり構文的な感じで書いてます...（笑）

　[2]：バリデーションをかけるクラスの名前の前に`@Validated`アノテーションを付けてください。
`BindingResult`もセットなので必ず付けるように。

　[3]：ここの`if文`でバリデーション(入力値エラーの有無)の判定をしています。
 正しく入力されていた場合は、次の登録内容確認画面へ遷移。
 一つでもエラーがあった場合は`showHome`メソッドにリターンし、再度入力画面を表示します。
 この時、入力に不備があったところにはエラーメッセージが記載されます。
    
　[4]：登録の確定ボタンが押下されたら入力内容をDBに保存します。
 保存はすべて`Service`側に任せているので、`UserService.java`に`UserForm`を投げます。
 そして、`UserService.java`は詰め替えなどを行い、DBに登録内容を保存します。

　[5]：登録が終わったら、そのセッションの中身をクリアします。それを`UserForm`で行います。
 クリアする理由としては、今回特に大きな理由はないが、実際のシステムで新規登録をしてもう一度新規登録画面を開くと、先ほど入力された値が入ったままになっているからクリアをして初期に戻す。

 ※文字が多くなって申し訳ない...

 ## 6.HTML,CSSの作成
 ・メインとなる入力画面
 ```HTML:index.html
 <!DOCTYPE html>
<html>
	<head>
		<meta charset="UTF-8">
		<title>Validation Sample</title>
		<link rel="stylesheet" th:href="@{/css/index.css}" />
	</head>
	<body>
		<div class="container">
	        <h1>Validation Sample</h1>
	        <form th:action="@{/confirmation}" method="post" th:object="${userForm}">
	            <p>ログインID(半角英数のみ)</p>
	            <div th:if="${#fields.hasErrors('loginId')}" th:errors="*{loginId}" class="error"></div>
	            <input type="text" id="loginId" name="loginId" th:value="*{loginId}"><br>
	            
	            <p>名前</p>
	            <div th:if="${#fields.hasErrors('name')}" th:errors="*{name}" class="error"></div>
	            <input type="text" id="name" name="name" th:value="*{name}"><br>
	            
	            <p>文字列(4文字以上10文字以内)</p>
	            <div th:if="${#fields.hasErrors('length')}" th:errors="*{length}" class="error"></div>
	            <input type="text" id="length" name="length" th:value="*{length}"><br>
	            
	            <p>メールアドレス</p>
	            <div th:if="${#fields.hasErrors('email')}" th:errors="*{email}" class="error"></div>
	            <input type="text" id="email" name="email" th:value="*{email}"><br>
	            
	            <input type="submit" value="確認">
	        </form>
	    </div>
	</body>
</html>
```
#### POINT
・Validationのメッセージの表示方法
`<form>`タグの中に`th:object="${userForm}"`を記載する。
各Validationのメッセージを表記させたい箇所に以下のコードを記入する。
`<div th:if="${#fields.hasErrors('loginId')}" th:errors="*{loginId}" class="error"></div>`

#### その他のHTML,CSSの記載
・入力内容確認画面
```html:user_confirm.html
<!DOCTYPE html>
<html>
	<head>
		<meta charset="UTF-8">
		<title>入力内容確認</title>
		<link rel="stylesheet" th:href="@{/css/user_confirm.css}" />
	</head>
	<body>
	    <div class="container">
	        <h1>入力内容確認</h1>
	        
	        <form th:action="@{/register}" method="post" th:object="${userForm}">
	            <p>ログインID(半角英数のみ)</p>
	            <hr>
	            <div th:text="*{loginId}" name="loginId" id="loginId" class="field-value"></div>
	            
	            <p>名前</p>
	            <hr>
	            <div th:text="*{name}" name="name" id="name" class="field-value"></div>
	            
	            <p>文字列(4文字以上10文字以内)</p>
	            <hr>
	            <div th:text="*{length}" name="length" id="length" class="field-value"></div>
	            
	            <p>メールアドレス</p>
	            <hr>
	            <div th:text="*{email}" name="email" id="email" class="field-value"></div>
	            
	            <input type="submit" value="確定">
	        </form>
	        
	        <form th:action="@{/back}" method="get">
	            <input type="submit" value="戻る" class="back-button">
	        </form>
	    </div>
	</body>
</html>
```

・登録完了画面
```html:complete.html
<!DOCTYPE html>
<html>
	<head>
		<meta charset="UTF-8">
		<title>完了画面</title>
	</head>
	<body>
		<div align="center">
			<h1>登録完了！！</h1>
			
			<form th:action="@{/back}" method="get">
				<input type="submit" value="ホームへ">
			</form>
		</div>
	</body>
</html>
```

・CSSは折りたたんでます。
<details><summary>index.css</summary>

```css:index.css
@charset "UTF-8";
body {
    font-family: Arial, sans-serif;
    background-color: #f7f7f7;
    margin: 0;
    padding: 0;
    display: flex;
    justify-content: center;
    align-items: center;
    height: 100vh;
}

.container {
    background-color: #ffffff;
    padding: 20px;
    border-radius: 10px;
    box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
    width: 300px;
    text-align: center;
}

h1 {
    font-size: 24px;
    margin-bottom: 20px;
    color: #333;
}

form {
    display: flex;
    flex-direction: column;
    align-items: center;
}

p {
    font-size: 14px;
    margin: 10px 0 5px 0;
    text-align: left;
    width: 100%;
    color: #555;
}

input[type="text"] {
    width: 100%;
    padding: 8px;
    margin: 5px 0 10px 0;
    border: 1px solid #ccc;
    border-radius: 4px;
    box-sizing: border-box;
}

input[type="submit"] {
    background-color: #4CAF50;
    color: white;
    padding: 10px 20px;
    border: none;
    border-radius: 5px;
    cursor: pointer;
    margin-top: 20px;
}

input[type="submit"]:hover {
    background-color: #45a049;
}

.error {
    color: red;
    font-size: 12px;
    margin-bottom: 10px;
}
```
</details>

<details><summary>user_confirm.css</summary>

```css:user_confirm.css
@charset "UTF-8";
body {
    font-family: Arial, sans-serif;
    background-color: #f7f7f7;
    margin: 0;
    padding: 0;
    display: flex;
    justify-content: center;
    align-items: center;
    height: 100vh;
}

.container {
    background-color: #ffffff;
    padding: 20px;
    border-radius: 10px;
    box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
    width: 300px;
    text-align: center;
}

h1 {
    font-size: 24px;
    margin-bottom: 20px;
    color: #333;
}

form {
    display: flex;
    flex-direction: column;
    align-items: center;
    margin-bottom: 10px;
}

p {
    font-size: 14px;
    margin: 10px 0 5px 0;
    text-align: left;
    width: 100%;
    color: #555;
}

hr {
    width: 100%;
    margin: 5px 0 10px 0;
    border: none;
    border-top: 1px solid #eee;
}

.field-value {
    width: 100%;
    padding: 8px;
    margin-bottom: 10px;
    background-color: #f1f1f1;
    border: 1px solid #ccc;
    border-radius: 4px;
    text-align: left;
}

input[type="submit"] {
    background-color: #4CAF50;
    color: white;
    padding: 10px 20px;
    border: none;
    border-radius: 5px;
    cursor: pointer;
    margin-top: 20px;
}

input[type="submit"]:hover {
    background-color: #45a049;
}

.back-button {
    background-color: #ccc;
    color: black;
}

.back-button:hover {
    background-color: #bbb;
}
```
</details>

## さいごに
まず、記事そのものが長くなってしまい申し訳ないです。
一人でも多く、Validationをうまく使っていただけたらと思います。
自分も全く完璧ではないので、何かアドバイス等があればよろしくお願いします。
最後まで読んでいただきありがとうございます。