【Spring Boot入門】#2 登録機能を作成する

## はじめに
自身の知識のアウトプットも兼ねて、新人研修用に作成した記事となります。Spring Bootを学び始めた方を対象とした内容になっています。

新規登録画面の作成と表示については、前回の記事を参照ください。

https://qiita.com/ioaio/items/b60055f5b334e8e313de

## 概要
企業情報の登録機能を作成していきます。
クライアントから渡ってきた企業情報をDBに追加し、最後に登録完了画面を返す機能を作成します。

#### 完成イメージ
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3917178/f199291d-ebd7-3c46-8e95-28ca693a0d3c.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3917178/aae161e8-f06c-27da-8c45-fe7edea0d97a.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3917178/a9eb9fb5-bdd5-6c35-7081-a029fd0b28d6.png)

## パッケージ構成
赤枠で囲ったパッケージ、クラス、HTMLファイルを今回作成していきます。また、青枠で囲ったクラスを編集していきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3917178/00c5cb26-6d2c-7ef1-dafb-1cfc15b0a6f3.png)

## Entityクラスの作成
このクラスは、データベースのテーブルをそのままミラーリングしたオブジェクトです。要は、データベースのテーブルのカラムと連携させる変数(フィールド)を持つクラスです。

ここでは、Formクラスがクライアントとのデータの受け渡しを担当するのに対し、Entityクラスはデータベースとのデータの受け渡しを担当します。
`配置先：src > main > java > com > example > demo > entity > Company.java`

```java:Company.java
package com.example.demo.entity;

import java.util.Date;

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Temporal;
import jakarta.persistence.TemporalType;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Entity
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Company {
	
	@Id
    @GeneratedValue(strategy=GenerationType.IDENTITY)
    private Long companyId;	// 企業ID

	@NotBlank
    private String name;	// 会社名
    
	@NotNull
    private int employees;	// 従業員数

    @Temporal(TemporalType.DATE)
    private Date establishmentDate;	// 設立日
    
}
```

#### @￰Entity
このクラスがJPAで利用するEntityクラスであることを宣言します。
データベースの１行と対になっていることを示します。

#### @￰Id
主キーを指定します。

#### @￰GeneratedValue(strategy = GenerationType.IDENTITY)
この記述により自動採番してくれます。MySQLの場合はAuto Increment(AI)として設定されます。

#### @￰NotBlank
必須チェックのためのアノテーションです。Null、空文字、半角スペースが許可されないことを示します。

#### @￰NotNull
必須チェックのためのアノテーションです。NotNullであることを示します。

#### @￰Temporal
日付型のオブジェクトの型を指定します。ここでは日付の部分のみ指定しています。(`TemporalType.DATE`)

Entityクラスを作成することで、下記のように接続情報がデータベースの自動更新を行う設定になっていれば、アプリケーション起動時に自動でEntityの定義に基づいてテーブルを作成してくれます。

```properties:application.properties
# Spring-JPA: DBのテーブルを自動作成してくれる機能
spring.jpa.hibernate.ddl-auto=update
```

試しにアプリケーションを起動してみると...

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3917178/ef871f8e-07f4-7672-4a37-6db1f535f180.png)


`sample`データベースにEntityクラスで指定した定義に基づいて`company`というテーブルが自動で作成されました。

#### 補足：
今回はEntityクラスの定義からテーブルを自動で作成しましたが、`@Table`アノテーションや`@Column`アノテーションを使うことで、テーブル名やカラム名、Null制約などを明示的に指定することもできます。

```java:Example
@Entity
@Data
@Table(name="tablename")
public class Company {
	
	@Id
    @GeneratedValue(strategy=GenerationType.IDENTITY)
    @Column(name = "id")
    private Long companyId;	// 企業ID

	@NotEmpty
    @Column(name = "company_name", length = 50, nullable = false)
    private String name;	// 会社名
    
	@NotNull
    @Column(name = "employees", length = 10, nullable = false)
    private int employees;	// 従業員数

    @Temporal(TemporalType.DATE)
    @Column(nullable = true)
    private Date establishmentDate;	// 設立日
    
}
```

`@Table`を省略した場合はクラス名を大文字にした名前のテーブルにマッピングされされ、`@Column`を省略した場合はプロパティ名を大文字にした名前のカラムへマッピングされます。（上述の通り）

:::note warn
`@Table` や `@Column` の name属性はキャメルケース(camelCase)ではなく、スネークケース(snake_case)で書きましょう。
もしスネークケースで書く場合は、`application.properties`に以下の１行を追加する必要があります。
```properties:application.properties
spring.jpa.hibernate.naming.physical-strategy=org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
```
この設定がないと、キャメルケースでカラム名やテーブル名を明示しても、裏で勝手にスネークケースに変換されるてしまう為、エラーの原因となります。
:::

## Repositoryクラスの作成
JpaRepositoryというインターフェイスを作成します。
これまでのクラス作成と違い、拡張インターフェイスの指定があるので注意しましょう。
`配置先：src > main > java > com > example > demo > repository > CompanyRepository.java`

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

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

import com.example.demo.entity.Company;

public interface CompanyRepository extends JpaRepository<Company, Long> {

}
```
JpaRepositoryは型とID型を指定する必要があります。
`T` は型なのでCompanyクラスを、`ID` はLongを指定します。

## Serviceクラスの作成
ビジネスロジックを実装します。このクラスは、コントローラーから呼び出されデータを処理します。

ここでは、コントローラーから渡ってきたFormのデータ（ブラウザからユーザーが入力した企業情報）をデータベースへ登録する為に、FormとEntity間のデータの変換を行いRepositoryクラスを呼び出します。

FormとEntity間のデータ変換の方法はいくつかありますが、今回は手動でマッピングします。明示的な変換ロジックをサービス層で記述します。

`配置先：src > main > java > com > example > demo > service > CompanyService.java`

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

import java.text.ParseException;

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

import com.example.demo.controller.form.CompanyForm;
import com.example.demo.entity.Company;
import com.example.demo.repository.CompanyRepository;

import jakarta.transaction.Transactional;

@Service
public class CompanyService {
	
	@Autowired
	CompanyRepository repository;
	@Autowired
	FormatService format;
	
	// 企業情報の新規登録
    @Transactional
	public Company createCompany(CompanyForm form) {
		Company entity = new Company();
		entity.setName(form.getCompanyName());
		entity.setEmployees(Integer.parseInt(form.getEmployees()));			
		try {
			entity.setEstablishmentDate(format.strToDate(form.getEst()));
		} catch (ParseException e) {
			System.err.println("Failed to convert format to date:" + e.getMessage());
			e.printStackTrace();
		}		
		return repository.save(entity);
	}

}
```

#### @￰AutoWired
これを付与することで、DIコンテナから対象のオブジェクトを取り出します。

DIコンテナには、@￰Component や @￰Controller、@￰Service、@￰Repositoryなどが付与されたクラスのインスタンスが登録され、コンテナ内で自動生成されます。簡単に言うと、使いたいクラスをインスタンス化をしてくれるアノテーションです。

ここでは`CompanyRepository`クラスを`repository`というインスタンス名で、`FormatService`クラスを`format`というインスタンス名で使えるように指定しています。

#### @￰Transactional
`@Transactional`を使用すると、メソッドが呼び出されると同時にトランザクションが開始され、メソッドの実行が成功した場合にはコミットされ、例外が発生した場合にはロールバックされます。これにより、データの整合性が保たれます。

#### repository.save(entity);
Form から Entity へデータの変換が出来たら、repositoryがデフォルトで持つ`save()`メソッドを利用して、EntityのデータをデータベースにINSERTします。save()はentityオブジェクトを引数にとり，それをDBに永続化します。

#### FormatService
FormとEntity間でデータを変換する際、今回で言うと`設立日`のようなFormとEntityで型が違うデータの場合には少し工夫が必要です。

ここでは `FormatService` というデータ型を変換する為のサービスクラスを作成し、文字列型 → 日付型 へ変換するロジックを定義します。

`配置先：src > main > java > com > example > demo > service > FormatService.java`

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

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;

import org.springframework.stereotype.Service;

@Service
public class FormatService {

	// String → Date
	public Date strToDate(String strDate) throws ParseException {
		SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
		Date date = dateFormat.parse(strDate);
		return date;
	}
}
```

## Viewの作成
thanks.htmlを作成します。
`配置先：src > main > resources > templates > thanks.html`

```java:thanks.html
<!DOCTYPE html>
<html lang="ja" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>企業情報登録完了画面</title>
</head>
<body>
	<main>
		<h1>登録完了</h1>
		<p>企業情報のご登録が完了いたしました。</p>
	</main>
</body>
</html>
```

## Controllerクラスの修正
CompanyController.javaに登録処理のメソッドを追加します。
`配置先：src > main > java > com > example > demo > controller > CompanyController.java`

```java:CompanyController.java
@Controller
public class CompanyController {
	
	@Autowired
	CompanyService companyService;

	// 登録フォームの表示
	
	// 企業情報の新規登録
	@PostMapping("company/register")
	public String addCompany(@ModelAttribute CompanyForm companyForm, Model model) {
		companyService.createCompany(companyForm);
		return "thanks";
	}

}
```
#### @￰PostMapping(path)
POSTリクエストのみをマッピングします。
@￰PostMappingアノテーションをつけることで、"/company/register"というURLにマッピングしています。

#### @￰ModelAttribute
Spring MVCにおいて、HTTPリクエストから受け取ったデータをモデルオブジェクトにバインドするためのアノテーションです。このアノテーションを使用することで、コントローラーのメソッドに引数として渡されたオブジェクトに、リクエストパラメータを自動的にマッピングできます。

今回は、クライアントからPOST送信されたデータをまとめてCompanyFormオブジェクトとして受け取っています。

#### companyService.createCompany(フォームオブジェクト);
`CompanyService`クラスの`createCompany`メソッドを呼び出しています。引数にはクライアントから渡ってきた`companyForm`オブジェクトを渡しています。

## データの流れ
登録処理のデータの流れは以下の通りです。
1. ユーザーが入力フォームに企業情報を入力して送信
2. `CompanyController`がフォームデータを受け取る
3. `CompanyService`に登録処理を依頼
4. `CompanyService`が必要な業務処理を行い(FormとEntity間のデータ変換)、`CompanyRepository`に保存を指示
5. `CompanyRepository`がデータベースに保存
6. 結果を順次上の層に返し、最終的にユーザーに結果を表示

次回はバリデーションチェックを実装していきます。

## 参考サイト
https://cs27.org/wiki/kobespiral2021/?SpringBoot/DTO

https://qiita.com/mtanabe/items/c879d233d297eda288d4

https://zenn.dev/ymasaoka/articles/use-camelcase-jpa-hibernate-sql-spring-boot

https://chat-messenger.com/blog/java/string-valueof-integer-parseint