Spring Bootを使ったREST APIの基本構築から動作確認まで③

## はじめに
本記事ではREST APIをデータベース（MySQL）と連携させ、基本的なCRUD操作を通じて、その動作の確認と例外処理についても説明していきます。

前回までの記事で、①使用する環境・ツールの基本構築から簡単な動作確認、及び②基本的なCRUD操作ができる簡単なREST APIを構築する方法について解説していますので、よければそちらもご覧ください。
[Spring Bootを使ったREST APIの基本構築から動作確認まで①](https://qiita.com/pomecode/items/dcd2353d1f3b49dc23ff)
[Spring Bootを使ったREST APIの基本構築から動作確認まで②](https://qiita.com/pomecode/items/2eb4fdd737264a335b1b)

## 対象者
- Javaの基礎知識を持っている方
- Spring Bootを初めて触る方、もしくは少し触ったことがある方
- REST APIについて学びたい方

## 動作環境
- Eclipse 2022（Pleiades All in One）
- Spring Boot 3.4.4
- Advanced REST Client
- MySQL 8.0.42

## 事前準備
本記事で使用する環境・ツールについて、インストール手順を記載していきます。
Eclipse（Pleiades All in One）、Spring Boot（Spring Initializr）、Advanced REST Clientについては前回までの記事で紹介していますので、そちらをご確認ください。
[Spring Bootを使ったREST APIの基本構築から動作確認まで①-事前準備](https://qiita.com/pomecode/private/dcd2353d1f3b49dc23ff#%E4%BA%8B%E5%89%8D%E6%BA%96%E5%82%99)
[Spring Bootを使ったREST APIの基本構築から動作確認まで②-事前準備](https://qiita.com/pomecode/items/2eb4fdd737264a335b1b#%E4%BA%8B%E5%89%8D%E6%BA%96%E5%82%99)

また、MySQLについてはこちらの記事をご参考ください。
[MySQLのインストール手順](https://qiita.com/pomecode/private/04bb1a4dc80b916315bd)

## データベース作成
MySQL Workbenchを使って本記事で使用するデータベースを作成します。

**手順**
1. Queryの入力欄に以下を入力して実行
    ```SQL
    create database TaskDB;
    ```
    問題なければ、画面下部に緑のチェックが表示されます。（以降も同様）
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4050193/594739db-95d3-4998-bc53-264ed968115e.png)
　
1. Queryの入力欄に以下を入力して実行
    ```SQL
    USE TaskDB;
    ```
    　
1. Navigator欄のSchemasタブでリフレッシュボタンを押下することで、作成したtaskdbが表示されます
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4050193/1ec6a0c7-92a8-4b62-ae69-6e0f4a97471d.png)


## 実装
EclipseでSpringのプロジェクトを作成し、実装していきましょう。
Spring Initializrを使用したプロジェクト作成については前々回の記事で紹介していますので、そちらをご確認ください。
[Spring Bootを使ったREST APIの基本構築から動作確認まで①-Spring Initializr](https://qiita.com/pomecode/private/dcd2353d1f3b49dc23ff#spring-initializr)

今回はプロジェクト名を"restapidemo3"として作成します。
依存性はSpring Webに加えて、MySQLに接続するため、MySQL Driver及びSpring Data JPAの計3つを選択してください。

**MySQL Driver**
その名の通り、Spring BootアプリケーションがMySQLと通信するために必要なソフトウェアです。

**Spring Data JPA**
データベースとのやり取りを簡単にするためのSpring Frameworkの一部です。SQLを書くことなく、シンプルなリポジトリインターフェースを使用してCRUD操作を行うことが可能となります。

### model
まずはmodelの実装です。
com.example.restapidemo3パッケージの配下にmodelパッケージを作成し、その中にTaskクラスを作成します。
前回記事で作成したTaskクラスを流用しますが、データベースに接続するため一部修正を加えています。
[Spring Bootを使ったREST APIの基本構築から動作確認まで②-model](https://qiita.com/pomecode/items/2eb4fdd737264a335b1b#model)

Taskクラスでは新たに3つのアノテーションを使用します。
**@&#xFFF0;Entity**
JPAエンティティを表すクラスに付与するアノテーションで、クラスがデータベースのテーブルにマッピングされます。
今回は名前にテーブル名：m_taskを指定しています。

**@&#xFFF0;Id**
エンティティの主キーとなるフィールドに付与するアノテーションです。
主キーの生成方法は通常、後述するGeneratedValueアノテーションで指定します。

**@&#xFFF0;GeneratedValue**
Idアノテーションと一緒に使用し、主キーが自動生成されることを指定します。
生成戦略をstrategy属性で指定しますが、今回はデータベースに依存する方法で主キーが生成される、GenerationType.IDENTITYを指定します。

また、主キーは自動採番されるため、以下2点も修正しておきます。
- idの型をStringからLongに変更
- 主キーの自動採番に伴い、コンストラクタでidを設定する必要がなくなったため、コンストラクタから削除

```java:Task.java
package com.example.restapidemo3.model;

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

@Entity(name="m_task")
public class Task {
	
	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private Long id;
	private String title;
	private String status;

	public Task() {

	}

	public Task(String title, String status) {
//		this.id = id;
		this.title = title;
		this.status = status;
	}

	public Long getId() {
		return id;
	}

	public void setId(Long id) {
		this.id = id;
	}

	public String getTitle() {
		return title;
	}

	public void setTitle(String title) {
		this.title = title;
	}

	public String getStatus() {
		return status;
	}

	public void setStatus(String status) {
		this.status = status;
	}
}
```

### repository
次にrepositoryです。
com.example.restapidemo3パッケージの配下にrepoパッケージを作成し、その中にTaskRepositoryインターフェースを作成します。
※クラスではなく、インターフェースです。

中身としては、CrudRepositoryを継承して、@&#xFFF0;Repositoryを付与するだけです。
CrudRepositoryはSpring Dataが提供するリポジトリインターフェースで、CRUD操作を簡単に実現できるようになります。
データベースにアクセスするには、このTaskRepositoryインターフェースを定義するのみでOKです。
こちらを実装するクラスはフレームワークが裏で自動的に生成してくれます。

**@&#xFFF0;Repository**
データアクセス層（DAO）であることを示すために使用されるアノテーションです。

```java:TaskRepository.java
package com.example.restapidemo3.repo;

import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Repository;

import com.example.restapidemo3.model.Task;

@Repository
public interface TaskRepository extends CrudRepository<Task, Long> {

}
```

### service
続いて、serviceの実装です。
同じく、com.example.restapidemo3パッケージの配下にserviceパッケージを作成し、その中にTaskServiceクラスを作成します。
前回記事で作成したTaskServiceクラスを流用しますが、こちらもデータベースに接続するため一部修正を加えます。
[Spring Bootを使ったREST APIの基本構築から動作確認まで②-service](https://qiita.com/pomecode/items/2eb4fdd737264a335b1b#service)

主な修正箇所は以下の3点です。
- データベースからデータ取得を行うため、タスク一覧のリストは削除
- TaskRepositoryクラスを使用するためにフィールドとして定義し、@&#xFFF0;Autowiredを付与して明示的にnewしなくとも使用できるように修正
- 各メソッドについて、taskRepositoryのfindAll()やfindById()などのメソッドを使用して、DBを操作して処理を行うように修正

```java:TaskService.java
package com.example.restapidemo3.service;

import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

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

import com.example.restapidemo3.model.Task;
import com.example.restapidemo3.repo.TaskRepository;

@Service
public class TaskService {
	
	@Autowired
	private	TaskRepository taskRepository;
	
//	private List<Task> allTasks = new ArrayList<> (Arrays.asList(
//			new Task("001", "掃除", "TODO"),
//			new Task("002", "洗濯", "DONE"),
//			new Task("003", "勉強", "IN_PROGRESS"),
//			new Task("004", "料理", "TODO"),
//			new Task("005", "買い物", "DONE")));

	public List<Task> getAllTasks(){
		List<Task> allTasks = new ArrayList<>();
		taskRepository.findAll().forEach(allTasks::add);
		
		return allTasks;
	}
	
	public Optional<Task> getTask(Long id) {
		return taskRepository.findById(id);
	}
	
	public void addTask(Task task) {
		taskRepository.save(task);
	}
	
	public void updateTask(Long id, Task task) {
		if(taskRepository.findById(id).get() != null) {
			taskRepository.save(task);
		}
	}
	
	public void deleteTask(Long id) {
		taskRepository.deleteById(id);
	}
}
```

### exception
次はexceptionの実装です。
com.example.restapidemo3パッケージの配下にexceptionパッケージを作成し、その中にTaskNotFoundExceptionクラスを作成します。
作成する際、スーパークラスはRuntimeExceptionを指定してください。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4050193/bb8c4124-5cad-46b5-b67c-11fd912aebc4.png)

クラスの中身としては、シリアライズされたデータとの互換性を保つためにserialVersionUIDを定義しておくのと、例外がスローされた際に指定されたIDを含むエラーメッセージを設定するため、コンストラクタを定義しておきます。

```java:TaskNotFoundException.java
package com.example.restapidemo3.exception;

public class TaskNotFoundException extends RuntimeException {
	
	private static final long serialVersionUID = 1L;
	
	public TaskNotFoundException(Long id) {
		super("タスクコード" + id + "は見つかりません。");
	}

}
```

exceptionパッケージの配下にもう一つ、例外をハンドルするためのクラスとしてTaskNotFoundExceptionControllerAdviceクラスを作成します。
ここでは、例外をハンドリングするためのメソッドを定義しておくのと、新たに4つのアノテーションを使用します。

**@&#xFFF0;ControllerAdvice**
アプリケーションの複数のコントローラーで共通するロジックをまとめるために付与するアノテーションで、主に例外処理を簡単に実現するために使用します。
例外処理においては、通常、後述するExceptionHandlerアノテーションと組み合わせます。

**@&#xFFF0;ResponseBody**
HTTPレスポンスのボディとして直接返すために使用されるアノテーションで、メソッドの戻り値をJSONやXML形式にシリアライズして返却します。

**@&#xFFF0;ExceptionHandler**
例外をハンドリングするためのアノテーションで、括弧内に指定した例外が発生した際にその例外をキャッチして、適切なレスポンスをクライアントに返してくれます。

**@&#xFFF0;ResponseStatus**
レスポンスのHTTPステータスコードを設定するためのアノテーションで、特に例外クラスに適用することで、その例外がスローされた際に特定のステータスコードを返すことができます。
ここではNOT_FOUNDを指定して、「404 Not Found」を返却するようにしています。

```java:TaskNotFoundExceptionControllerAdvice.java
package com.example.restapidemo3.exception;

import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;

@ControllerAdvice
public class TaskNotFoundExceptionControllerAdvice {
	
	@ResponseBody
	@ExceptionHandler(TaskNotFoundException.class)
	@ResponseStatus(HttpStatus.NOT_FOUND)
	public String taskNotFoundHandler(TaskNotFoundException ex) {
		return ex.getMessage();
	}

}
```

### controller
最後に、controllerの実装です。
com.example.restapidemo3パッケージの配下にcontrollerパッケージを作成し、その中にTaskControllerクラスを作成します。
こちらも、前回記事で作成したTaskControllerクラスを流用しますが、一部修正を加えます。
[Spring Bootを使ったREST APIの基本構築から動作確認まで②-controller](https://qiita.com/pomecode/items/2eb4fdd737264a335b1b#controller)

修正箇所は以下の2点です。
- Taskクラスでidの型を変更したため、StringからLongに修正
- taskServiceのgetTaskメソッドがOptional型を返すように修正したため、.orElseThrowを追加して、もしnullが返ってきた場合にカスタム例外をスローするように修正

```java:TaskController.java
package com.example.restapidemo3.controller;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
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.RequestBody;
import org.springframework.web.bind.annotation.RestController;

import com.example.restapidemo3.exception.TaskNotFoundException;
import com.example.restapidemo3.model.Task;
import com.example.restapidemo3.service.TaskService;

@RestController
public class TaskController {

	@Autowired
	private TaskService taskService;

	@GetMapping("/tasks")
	public List<Task> getAllTasks() {
		return taskService.getAllTasks();
	}

	@GetMapping("/tasks/{id}")
	public Task getTask(@PathVariable("id") Long id) {
		return taskService.getTask(id).orElseThrow(() -> new TaskNotFoundException(id));
	}

	@PostMapping("/tasks")
	public void addTask(@RequestBody Task task) {
		taskService.addTask(task);
	}

	@PutMapping("/tasks/{id}")
	public void updateTask(@RequestBody Task task, @PathVariable("id") Long id) {
		taskService.updateTask(id, task);
	}

	@DeleteMapping("/tasks/{id}")
	public void deleteTask(@PathVariable("id") Long id) {
		taskService.deleteTask(id);
	}
}
```

以上で処理の実装は完了となりますが、演習用データの投入とデータベースの接続情報設定のため、以下にコードを追加します。

### Restapidemo3Application
アプリケーションの起動時にMySQLのtaskdb上に初期値が登録されるようにしておきます。

```java:Restapidemo3Application.java
package com.example.restapidemo3;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

import com.example.restapidemo3.model.Task;
import com.example.restapidemo3.repo.TaskRepository;

@SpringBootApplication
public class Restapidemo3Application implements CommandLineRunner{

	@Autowired
	private TaskRepository taskRepository;
	
	public static void main(String[] args) {
		SpringApplication.run(Restapidemo3Application.class, args);
	}
	
	@Override
	public void run(String... args) throws Exception {
		taskRepository.save(new Task("掃除", "TODO"));
		taskRepository.save(new Task("洗濯", "DONE"));
		taskRepository.save(new Task("勉強", "IN_PROGRESS"));
		taskRepository.save(new Task("料理", "TODO"));
		taskRepository.save(new Task("買い物", "DONE"));
	}

}
```

### application.properties
データベースの接続情報を設定します。
passwordは各自で設定したものを入力してください。
その他について、一部解説しておきます。

**spring.jpa.hibernate.ddl-auto**
アプリケーションの起動時にデータベースのスキーマをどのように操作するかを制御します。
createを指定した場合、既存のスキーマが無ければ作成し、もしあるようであれば削除して再作成します。

**spring.jpa.show-sql**
SQLクエリをコンソールに表示するための設定で、trueにした場合、実行されるクエリをコンソール上で確認することができます。

**spring.jpa.properties.hibernate.format_sql**
出力されるSQLクエリを整形された形で表示するための設定で、trueにした場合、クエリが見やすい形式で表示されます。


```properties:application.properties
spring.application.name=restapidemo3
spring.datasource.url=jdbc:mysql://localhost:3306/TaskDB
spring.datasource.username=root
spring.datasource.password=各自で設定したパスワード
spring.jpa.hibernate.ddl-auto=create
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true
```

## 動作確認
ここまで出来たら、アプリケーションを起動してみましょう。
起動すると、コンソールに以下のように表示されているかと思います。
※テーブルの初期化とデータ挿入

```
Hibernate: 
    drop table if exists m_task
Hibernate: 
    create table m_task (
        id bigint not null auto_increment,
        status varchar(255),
        title varchar(255),
        primary key (id)
    ) engine=InnoDB
```
```
Hibernate: 
    insert 
    into
        m_task
        (status, title) 
    values
        (?, ?)
```

### MySQL Workbench
MySQL Workbenchでも確認してみましょう。
Navigator欄右上のリフレッシュボタンを押下すると、taskdb-Tablesの下にm_taskが作成されているかと思います。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4050193/ab861018-db0e-4991-be65-ddfdc16312e4.png)

また、以下のSQLを実行すると、m_taskテーブルに5レコード追加されていることが分かります。

```sql
select *
from m_task;
```
**実行結果**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4050193/b9931c74-b9b7-4c7c-a59e-6025ceda46c7.png)

### REST Client
続いて、REST Clientでも確認してみましょう。
操作方法については前回記事で説明していますので割愛します。
[Spring Bootを使ったREST APIの基本構築から動作確認まで②-動作確認](https://qiita.com/pomecode/items/2eb4fdd737264a335b1b#%E5%8B%95%E4%BD%9C%E7%A2%BA%E8%AA%8D)

#### POST
新たにタスクを1件追加します。

メソッド：POST
URL：http://localhost:8080/tasks
形式：JSON

```json
{
  "title": "運動",
  "status": "TODO"
}
```
idは自動採番されるため、指定不要です。
上記を実行後、MySQL Workbenchで確認してみます。
```sql
select *
from m_task;
```
**実行結果**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4050193/9eb9ad4f-db5e-4de8-98e0-8f3928086cf7.png)

id：6にPOSTしたデータが追加されました。

#### PUT
id：1のタスクのステータスをDONEに更新します。

メソッド：PUT
URL：http://localhost:8080/tasks/1
形式：JSON

```json
{
  "id":1,
  "title": "掃除",
  "status": "DONE"
}
```
上記を実行後、MySQL Workbenchで確認してみます。
```sql
select *
from m_task;
```
**実行結果**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4050193/fde9fd41-018b-47cc-93aa-2439dc69ca2e.png)

id：1のステータスがPUTした内容に更新されました。

#### DELETE
先程追加したid：6のタスクを削除します。

メソッド：DELETE
URL：http://localhost:8080/tasks/6

上記を実行後、MySQL Workbenchで確認してみます。
```sql
select *
from m_task;
```
**実行結果**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4050193/b1fae342-cdf1-4858-abd2-4148e8d9147a.png)

id：6のタスクがDELETEメソッドによって削除されました。

### 例外の確認
最後に例外の確認をしておきましょう。
REST ClientでGETメソッドを選択し、URLに存在しないidを指定します。
ここでは、先程削除したid：6を指定してみます。

メソッド：GET
URL：http://localhost:8080/tasks/6

実行すると、以下のようにHTTPステータスコード：404と、エラーメッセージが表示されることが確認できました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4050193/0639e563-876e-4aa3-b6e5-9acd9bdaa598.png)