Spring bootで簡単なWebアプリを作ってみる(ロジック作成1)

こんにちは。船井総研デジタルのいっちーです。
前回の記事では、画面遷移を作成しました。

https://qiita.com/fsd_itchy/items/b85a52a28fd925027791

今回は、ロジックを組み込んで、画面(フロントサイド)とサーバ(バックサイド)でデータの授受ができるようにしていきます。長くなると思いますので、何回かに分けて投稿したいと思います。

# データのやり取りを考える
画面遷移の際に、どのようなデータがやり取りされるかを考えます。

## メイン画面
メイン画面とサーバの間でやり取りされるデータを洗い出していきます。

![mainscreen.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3280429/3c32d1aa-8fc0-c86c-4ef3-d49be52def86.png)

### サーバーから画面に送られるデータ

| No | データ | 繰り返し | 備考 |
|:-:|:-|:-|:-|
| 1 | つぶやき | あり  | - |
| 2 | エラーメッセージ | あり | 処理時にエラーが発生した場合のみ。エラーメッセージがなければ表示エリアそのものを出力しない。 |
| 3 | ページ情報 | なし | - |

### 画面からサーバーに送られるデータ

| No | データ | 繰り返し | 備考 |
|:-:|:-|:-|:-|
| 4 | つぶやき | なし | 処理時にエラーが発生した場合、表示中のページに戻すため、自ページ番号を一緒に送る。 |

## ねぎ登録画面
ねぎ登録画面とサーバの間でやり取りされるデータを洗い出していきます。

![registerscreen.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3280429/65d6073d-a7a5-b493-81e7-581012783c75.png)

:::note info
ねぎマスタの主キーであるIDを非表示項目として画面に渡すことで、対応する「修正」「削除」のボタンを押したときに、対応するねぎ情報のIDをサーバーに返せるようにします。
:::

### サーバーから画面に送られるデータ

| No | データ | 繰り返し | 備考 |
|:-:|:-|:-|:-|
| 1 | ねぎ情報 | あり  | - |
| 2 | エラーメッセージ | あり | 処理時にエラーが発生した場合のみ。エラーメッセージがなければ表示エリアそのものを出力しない。 |

### 画面からサーバーに送られるデータ

| No | データ | 繰り返し | 備考 |
|:-:|:-|:-|:-|
| 3 | 登録情報 | なし  | - |
| 4 | 更新情報 | なし | ボタンの種類で修正/削除を区別し、ねぎマスタのIDを送ることで修正/削除対象のレコードを特定する。 |

# データ受け渡しのためのクラスを作成する。

画面とサーバーの間でデータを受け渡すためのクラスを実装していきます。

## 画面からサーバーに送られるデータ

画面からサーバーに送られるデータは、画面の`form`に対応したFormクラスで表現します。前準備として、データのバリデーションを自動的に組み込めるように、pom.xmlのdependenciesに下記の2つを追加します。

```xml:pom.xml

(略)

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-validation</artifactId>
	<version>3.0.4</version>
</dependency>
<dependency>
	<groupId>org.springframework.data</groupId>
	<artifactId>spring-data-commons</artifactId>
	<version>3.0.3</version>
</dependency>

(略)

```

<details><summary>つぶやきForm</summary>

```diff_java:src/main/java/com/example/negitter/form/NegiitForm.java
@Data
@AllArgsConstructor
public class NegiitForm {

    @NotBlank(message="お名前は必須項目です。")
    @Size(max=128, message="お名前は128文字以下で入力してください。")
    private String name;
    @NotBlank(message="つぶやきは必須項目です。")
    @Size(max=128, message="つぶやきは128文字以下で入力してください。")
    private String negiit;
    private Integer pageNum;

}
```
</details>

<details><summary>ねぎ登録Form</summary>

```diff_java:src/main/java/com/example/negitter/form/RegisterNegiForm.java
@Data
@AllArgsConstructor
public class RegisterNegiForm {

    @NotBlank(message="ねぎ名称は必須項目です。")
    @Size(max=128, message="ねぎ名称は128文字以下で入力してください。")
    private String negiName;

}
```
</details>

<details><summary>ねぎ更新Form</summary>

```diff_java:src/main/java/com/example/negitter/form/UpdateNegiForm.java
@Data
@AllArgsConstructor
public class UpdateNegiForm {

    @NotBlank(message="処理区分が設定されていません。")
    private String process;
    @NotNull(message="処理対象が設定されていません。")
    private Integer id;
    @NotBlank(message="ねぎ名称が入力されていません。")
    @Size(max=128, message="ねぎ名称は128文字以下で入力してください。")
    private String negiName;

}
```
</details>

各フィールドに`@NotNull`等のアノテーションをつけることによって、項目チェックを組み込むことができます。バリデーション結果は、Controllerクラスのメソッドに`BindingResult`型の引数を定義することで受け取ることができます。

## サーバーから画面に送られるデータ

サーバーから画面に送られるデータのうち、つぶやきとねぎ情報の2つは、DBテーブルレコードのListとなります。今回MyBatisを使ってO/Rマッピングを行いますので、MyBatis Generatorで必要なソースを自動作成します。

### pom.xmlの修正

pom.xmlのpluginsに下記の設定を追加します。

```xml:pom.xml

(略)

<plugin>
	<groupId>org.mybatis.generator</groupId>
	<artifactId>mybatis-generator-maven-plugin</artifactId>
	<version>1.4.1</version>
	<configuration>
		<configurationFile>
			${project.basedir}/src/resources/dev/generatorConfig.xml</configurationFile>
		<overwrite>
			true</overwrite>
		<includeAllDependencies>true</includeAllDependencies>
	</configuration>
	<dependencies>
		<dependency>
			<groupId>com.softwareloop</groupId>
			<artifactId>mybatis-generator-lombok-plugin</artifactId>
			<version>1.0</version>
		</dependency>
	</dependencies>
</plugin>

(略)

```

configurationFileは、MyBatis Generatorの設定ファイルを格納するパスを指定します。続いて、MyBatis Generatorの設定ファイルを作ります。

### MyBatis Generatorの設定
<details><summary>generatorConfig.xml</summary>

```xml:src/resources/dev/generatorConfig.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE generatorConfiguration PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN" "http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">
<generatorConfiguration>
    <context id="tables" targetRuntime="MyBatis3">
        <!-- <plugin type="org.mybatis.generator.plugins.EqualsHashCodePlugin"/> -->
        <plugin type="org.mybatis.generator.plugins.MapperAnnotationPlugin" />
        <plugin type="com.softwareloop.mybatis.generator.plugins.LombokPlugin" />
        <jdbcConnection
            driverClass="com.mysql.cj.jdbc.Driver"
            connectionURL="jdbc:mysql://localhost/negitter"
            userId="user"
            password="pass" />

        <javaModelGenerator
            targetPackage="com.example.negitter.entity"
            targetProject="MAVEN" />
        <sqlMapGenerator
            targetPackage="com.example.negitter.mapper"
            targetProject="MAVEN" />
        <javaClientGenerator
            type="XMLMAPPER"
            targetPackage="com.example.negitter.mapper"
            targetProject="MAVEN" />

        <table tableName="negi_mst" enableInsert="true" enableSelectByPrimaryKey="true"
            enableSelectByExample="false" enableUpdateByPrimaryKey="true"
            enableDeleteByPrimaryKey="true" enableDeleteByExample="false"
            enableCountByExample="false" enableUpdateByExample="false"
            selectByPrimaryKeyQueryId="false" selectByExampleQueryId="false">
            <generatedKey column="id" sqlStatement="JDBC" />
        </table>
        <table tableName="message_tbl" enableInsert="true"
            enableSelectByPrimaryKey="true"
            enableSelectByExample="false" enableUpdateByPrimaryKey="true"
            enableDeleteByPrimaryKey="true" enableDeleteByExample="false"
            enableCountByExample="false" enableUpdateByExample="false"
            selectByPrimaryKeyQueryId="false" selectByExampleQueryId="false">
            <generatedKey column="id" sqlStatement="JDBC" />
        </table>
    </context>
</generatorConfiguration>
```
</details>

### ソース生成

VSCodeのMAVENタブを開いていくとmybatis-generatorという項目が追加されていると思いますので、「generate」を実行します。

![mybatisgenerate.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3280429/e098f742-6a51-58ed-9095-baef9fb68c02.png)

実行が終わると、targetフォルダ配下に自動生成されたentitiyクラス、mapperクラス、mapper.xmlが格納されますので、これをsrcフォルダ内のしかるべきパスに配置します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3280429/e33d0a41-606b-b1e4-0896-b4c717da11c6.png)

::: note warn
後の工程でこれらのソースを手書きで修正していきますが、手書き修正したソースと自動生成したソースが混在すると、デグレしたりして管理が面倒なことになるので、十分お気をつけください。
:::

# 受け渡しデータの定義をControllerクラスに追加する

設計したデータ定義に基づいて、Controllerクラスを修正します。

## 画面からサーバーに送られるデータ

ControllerクラスのPost/GetMappingしているメソッドに引数を追加します。それぞれ以下のような意味合いがあります。

| 型 | アノテーション | 役割 | 備考 |
|:-|:-|:-|:-|
| Model | - | サーバーから画面へ引き渡すデータを格納する | - |
| T | `@RequestParam` | リクエストパラメータを格納する | - |
| T | `@ModelAttribute` | 画面のFormデータを格納する | - |
| T | `@Valid` | 自動的にバリデーションされるデータであることを示す | BindingResultでバリデーション結果を受け取る前提 |
| BindingResult | - | バリデーション結果を格納する | 対象のFormクラスが`@Valid`で修飾されている前提 |

<details><summary>Controllerクラス</summary>

```diff_java:src/main/java/com/example/negitter/controller/NegitterController.java

(略)

@GetMapping("/")
-public String index() {
+public String index(@RequestParam(name = "page", required = false, defaultValue = "1") int pageNum, Model model) {
	return "negitter";
}

(略)

@GetMapping("/register")
-public String register() {
+public String register(Model model) {
	return "register";
}

(略)

@GetMapping("/negitter")
-public String negitter() {
+public String negitter(@RequestParam(name = "page", required = false, defaultValue = "1") int pageNum, Model model) {
	return "negitter";
}

(略)

@PostMapping("/negiit")
-public String negiit() {
+public String negiit(@ModelAttribute @Valid NegiitForm negiitForm, BindingResult bindingResult, Model model) {
	return "negitter";
}

(略)

@PostMapping("/registerNegi")
-public String registerNegi() {
+public String registerNegi(@ModelAttribute @Valid RegisterNegiForm negiForm, BindingResult bindingResult, Model model) {
	return "register";
}

(略)

@PostMapping("/updateNegi")
-public String updateNegi() {
+public String updateNegi(@ModelAttribute @Valid UpdateNegiForm updateNegiForm, BindingResult bindingResult, Model model) {
	return "register";
}

(略)

```
</details>

## サーバーから画面に送られるデータ

Post/GetMappingメソッドに追加したModel型の引数に画面に引き渡すデータを格納します。
データの中身は次回以降作成していきますので、バリデーション結果以外は仮の値をとりあえず詰めておくことにします。

<details><summary>Controllerクラス</summary>

```diff_java:src/main/java/com/example/negitter/controller/NegitterController.java
(略)

@GetMapping("/")
public String index(@RequestParam(name = "page", required = false, defaultValue = "1") int pageNum, Model model) {
+    // TODO つぶやきとページ情報を取得
+    model.addAttribute("negiits", new ArrayList<MessageTbl>());
+    model.addAttribute("pageNum", pageNum);
+    model.addAttribute("totalPages", 1);
+    model.addAttribute("prevPage", 1);
+    model.addAttribute("nextPage", 1);
+    model.addAttribute("errorMessages", new ArrayList<String>());
	return "negitter";
}

(略)

@GetMapping("/register")
public String register(Model model) {
+    // TODO ねぎマスタのデータを取得
+    model.addAttribute("negiList", new ArrayList<NegiMst>());
+    model.addAttribute("errorMessages", new ArrayList<String>());
	return "register";
}

(略)

@GetMapping("/negitter")
public String negitter(@RequestParam(name = "page", required = false, defaultValue = "1") int pageNum, Model model) {
+    // TODO つぶやきとページ情報を取得
+    model.addAttribute("negiits", new ArrayList<MessageTbl>());
+    model.addAttribute("pageNum", pageNum);
+    model.addAttribute("totalPages", 1);
+    model.addAttribute("prevPage", 1);
+    model.addAttribute("nextPage", 1);
+    model.addAttribute("errorMessages", new ArrayList<String>());
	return "negitter";
}

(略)

@PostMapping("/negiit")
public String negiit(@ModelAttribute @Valid NegiitForm negiitForm, BindingResult bindingResult, Model model) {
+    List<String> errorMessages = bindingResult.getAllErrors().stream().map(e -> e.getDefaultMessage()).toList();    
+    // TODO つぶやき登録処理結果によってページ情報を設定
+    model.addAttribute("negiits", new ArrayList<MessageTbl>());
+    model.addAttribute("pageNum", pageNum);
+    model.addAttribute("totalPages", 1);
+    model.addAttribute("prevPage", 1);
+    model.addAttribute("nextPage", 1);
+    model.addAttribute("errorMessages", errorMessages);	
+    return "negitter";
}

(略)

@PostMapping("/registerNegi")
public String registerNegi(@ModelAttribute @Valid RegisterNegiForm negiForm, BindingResult bindingResult, Model model) {
+    List<String> errorMessages = bindingResult.getAllErrors().stream().map(e -> e.getDefaultMessage()).toList();
+    // TODO ねぎ情報登録結果を設定
+    // TODO ねぎ情報を検索し直す
+    model.addAttribute("negiList", negiList);
+    model.addAttribute("errorMessages", errorMessages);
	return "register";
}

(略)

@PostMapping("/updateNegi")
public String updateNegi(@ModelAttribute @Valid UpdateNegiForm updateNegiForm, BindingResult bindingResult, Model model) {
+    List<String> errorMessages = bindingResult.getAllErrors().stream().map(e -> e.getDefaultMessage()).toList();
+    // TODO ねぎ情報更新結果を設定
+    // TODO ねぎ情報を検索し直す
+    model.addAttribute("negiList", negiList);
+    model.addAttribute("errorMessages", errorMessages);
	return "register";
}

+ // TODO ページ情報を管理するprivate methodを作る

(略)
```
</details>

# まとめ
今回は、サーバーと画面の間でやり取りされるデータの設計と、必要なクラス等の実装を行いました。
次回は、受け取ったデータを画面側で表示できるように、テンプレートを修正していきます。

それではまた。