spring boot + spring jpaでデータベースに接続しCRUD操作

# はじめに

- 前回、[spring boot で web api サーバを作る](https://qiita.com/kuro227/items/d7da647e9f3be78a5f92) で文字列を返すだけの api を作りました
- 今回は前回のコードをベースに、データの CRUD（Create, Read, Update, Delete）操作を行う api を作ります
- データベースは[ローカル環境に docker-compose で mysql コンテナを立ち上げる](https://qiita.com/kuro227/items/d00e3cad875347a2cb9f) で作成した mysql コンテナを使います
- エラーハンドリングなどは考えず、一旦動く機能を実装します

# 環境

- macOS Mojave 10.14.6
- Java openjdk 12.0.1
- Spring Boot 2.1.7
- Intellij IDEA CE 2019.1
- docker 18.09.2
- docker-compose 1.24.1

# 取り扱うデータ

- 簡易版ツイッターのバックエンドサーバのようなイメージで、以下のデータを扱います

| 項目名  |   型   | 説明                 |
| :-----: | :----: | :------------------- |
|   id    |  int   | ツイートに固有の ID  |
|  name   | String | ツイートした人の名前 |
| message | String | ツイート内容         |

# 作成する api のイメージ

| url         | HTTP メソッド | 説明                             |
| :---------- | :------------ | :------------------------------- |
| /tweet      | POST          | ツイートを保存する               |
| /tweet      | GET           | 全ツイートを取得する             |
| /tweet/{id} | PUT           | 指定した id のツイートを更新する |
| /tweet/{id} | DELETE        | 指定した id のツイートを削除する |

# パッケージ構成

- 今回は以下のようなパッケージ構成でコードを作成していきます

```
src/main/java/com/example/api
.
├── ApiApplication.java
├── model
│   └── Tweet.java
├── repository
│   └── TweetRepository.java
├── service
│   └── TweetService.java
└── controller
    └── TweetController.java
```

- `model` : エンティティ（取り扱うデータそのもの）を表す
- `repository` : DB へのアクセスを行う
- `service` : ビジネスロジックを実行する
- `controller` : クライアントからのリクエスト/レスポンスを処理する

# build.gradle の設定

- データベースとデータのやり取りを行いやすくするためのライブラリを追加します
- build.gradle の dependencies に以下を追加します
  - `org.springframework.boot:spring-boot-starter-data-jpa`  
    RDB を java で操作するための api（JPA : Java Persistence API）を提供するライブラリ
  - `mysql:mysql-connector-java`  
    java で mysql データベースに接続するための api（JDBC : Java Database Connectivity）を提供するライブラリ
  - `org.projectlombok:lombok`  
    java で getter / setter やコンストラクタ等のよく使うコードを自動生成してくれるライブラリ  
    [Lombok メモ](https://qiita.com/kuro227/items/3f017bc01a478d9cd139) も参照ください

```build.gradle
	implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
	implementation 'mysql:mysql-connector-java'

	compileOnly 'org.projectlombok:lombok:1.18.8'
	annotationProcessor 'org.projectlombok:lombok:1.18.8'
```

# DB の接続設定

- `src/main/resources`の下に、`application.properties` または `application.yml` を作成し、接続する DB の情報を記載します

```yaml:application.yml
spring:
  datasource:
    sqlScriptEncoding: UTF-8
    driverClassName: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://127.0.0.1:3306/testdb?useSSL=false&requireSSL=false
    username: root
    password: hoge
  # アプリケーション起動時にデータベースを初期化しないようにする
  jpa:
    hibernate:
      ddl-auto: update
    # spring boot起動時にDBにテーブル作成できない問題の対応
    properties:
      hibernate:
        dialect: org.hibernate.dialect.MySQL57Dialect
logging:
  level:
    sql: debug
```

- `spring.jpa.properties.hibernate.dialect` の設定を入れているのはアプリケーション起動時にテーブルを作成できずに以下のようなエラーが発生したためです。[こちら](https://stackoverflow.com/questions/52574685/spring-boot-hibernate-fails-creating-table)を参考に解決しました

```
org.hibernate.tool.schema.spi.CommandAcceptanceException: Error executing DDL "create table tweet (tweet varchar(255) not null, primary key (tweet)) engine=MyISAM" via JDBC Statement
...（略）...
Caused by: java.sql.SQLSyntaxErrorException: Specified key was too long; max key length is 1000 bytes
...（略）...
```

# Create / Read

- まずは tweet をデータベースに保存/取得する機能を実装します
- Create 時はリクエストボディ内の `name`と`message` を DB に保存し、保存した内容をそのままレスポンスとして返します
- Get 時は DB に格納されているデータを全て取得し、レスポンスとして返します

## model の作成

- `model` パッケージ配下に、ツイート自体を表すクラスを作成します

```java:Tweet.java
package com.example.api.model;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import javax.persistence.*;

@Entity
@Data
@NoArgsConstructor
@AllArgsConstructor
@Table(name = "tweet")
public class Tweet {
    @Id
    @GeneratedValue
    private int id;

    @Column(nullable = false)
    private String name;

    @Column(nullable = false)
    private String message;
}
```

- `@Entity` : JPA のエンティティ（DB に保存するオブジェクト）であることを表す
- `@Data / @NoArgsConstructor / @AllArgsConstructor` : lombok のアノテーション。getter, setter, コンストラクタなどを自動作成する
- `@Table(name = "tweet")` : エンティティに対応するテーブル名を指定する
- `@Id` : エンティティの主キーであることを表すアノテーション
- `@GeneratedValue` : これをつけると DB で自動採番されるようになる
- `@Column(nullable = false)` : DB のカラムに付与するアノテーション。nullable オプションで null を許容するかどうかを設定できる

## repository の作成

- `repository` パッケージ配下にリポジトリクラスを作成します
- CRUD 操作に必要なメソッドは JpaRepository に含まれているため、JpaRepository を継承したインターフェースを作成するだけで OK です

```java:TweetRepository.java
package com.example.api.repository;

import com.example.api.model.Tweet;
import org.springframework.data.jpa.repository.JpaRepository;

public interface TweetRepository extends JpaRepository<Tweet, Integer> {}
```

## service の作成

- `service` パッケージ配下にサービスクラスを作成します
- tweetRepository に `@Autowired` を付けることでインスタンスが DI コンテナから渡され、new することなくメソッドを呼び出せるようになります
- postTweet メソッドで、Tweet を受け取り、tweetRepository の save メソッドを呼び出しています
- getTweet メソッドで、tweetRepository の findAll メソッドを呼び出しています。

```java:TweetService.java
package com.example.api.service;

import com.example.api.model.Tweet;
import com.example.api.repository.TweetRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

@Service
@Transactional
public class TweetService {
    @Autowired
    TweetRepository tweetRepository;

    public Tweet postTweet(Tweet tweet) {
        return tweetRepository.save(tweet);
    }

    public List<Tweet> getTweet() {
        return tweetRepository.findAll();
    }
}
```

- `@Service` : spring MVC のサービスクラスであることを表す。DI (Dependency Injection) の対象となる
- `@Transactional` : トランザクション制御を行うためのアノテーション
- `@Autowired` : DI コンテナのインジェクション対象に付けるアノテーション

## controller の作成

- `controller`パッケージ配下にコントローラクラスを作成します
- `@RequestMapping()`アノテーションを使い、`tweet`というパスに対して POST メソッドで呼び出された場合は postTweet メソッドを、GET メソッドで呼び出された場合は getTweet メソッドが呼び出されるようにしています
- postTweet メソッドでは `@RequestBody` アノテーションを使って HTTP リクエストのボディを Tweet オブジェクトにマッピングし、tweetService の postTweet メソッドに渡しています。tweetService.postTweet()の実行後、結果を返却しています

```java:TweetController.java
package com.example.api.controller;

import com.example.api.model.Tweet;
import com.example.api.service.TweetService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequestMapping("tweet")
public class TweetController {
    @Autowired
    TweetService tweetService;

    @RequestMapping(method = RequestMethod.POST)
    Tweet postTweet(@RequestBody Tweet tweet) {
        return tweetService.postTweet(tweet);
    }

    @RequestMapping(method = RequestMethod.GET)
    List<Tweet> getTweet() {
        return tweetService.getTweet();
    }
}
```

- `@RestController` : コントローラクラスに付与するアノテーション
- `@RequestMapping()` : HTTP リクエストを受け付けるためのアノテーション。リクエストを受け付けるパスや HTTP メソッドを指定する
- `@RequestBody` : リクエストのボディを指定したオブジェクトにマッピングするアノテーション

## 動作確認

- アプリケーションを起動し、ターミナルから以下のコマンドを実行します

```shell
# post request
$ curl -X POST -H 'Content-Type:application/json' -d '{"name":"名前", "message":"メッセージ"}' localhost:8080/tweet

# response
{"id":1,"name":"名前","message":"メッセージ"}
```

- データベースを確認し、リクエストした内容が保存されていれば OK です

```
mysql> select * from tweet;
+----+-----------------+--------+
| id | message         | name   |
+----+-----------------+--------+
|  1 | メッセージ      | 名前   |
+----+-----------------+--------+
1 row in set (0.01 sec)
```

- 次に、データベースの内容が取得できるか確認するため、ターミナルから以下のコマンドを実行します

```
# get request
curl -X GET -H 'Content-Type:application/json' localhost:8080/tweet

# response
[{"id":1,"name":"名前","message":"メッセージ"}]
```

- データベース内のレコードを取得できました！

# Update / Delete

- 続いて、`/tweet/{id}` で指定された id の tweet を更新/削除する機能を実装します
- JpaRepository には update/delete に用いるメソッドが既に実装されているため、repository の修正は不要です。（service から呼び出すだけ）

## service の修正

- TweetService.java のクラス内に以下のメソッドを追加します

```java:TweetService.java
    public Tweet updateTweet(Tweet tweet) {
        return tweetRepository.save(tweet);
    }

    public void deleteTweet(Integer id) {
        tweetRepository.deleteById(id);
    }
```

- update は create と同じ `save()`メソッドを用います
- delete は id で指定したエンティティを削除する `deleteById()` メソッドを用います

## controller の修正

- TweetController.java のクラス内に以下のメソッドを追加します

```java:TweetController.java
    @RequestMapping(value = "{id}", method = RequestMethod.PUT)
    Tweet putTweet(@PathVariable("id") Integer id, @RequestBody Tweet tweet) {
        tweet.setId(id);
        return tweetService.updateTweet(tweet);
    }

    @RequestMapping(value = "{id}", method = RequestMethod.DELETE)
    void deleteTweet(@PathVariable("id") Integer id) {
        tweetService.deleteTweet(id);
    }
```

- `@RequestMapping` の value オプションに `{id}` を指定し、`/tweet/{id}` でリクエストされた場合に該当のメソッドが呼ばれるように設定します
- `@PathVariable` アノテーションで URL 内の変数 {id} を Integer の id という変数にマッピングします
- 更新時は、id を setTweet()メソッドでセットしてから tweetService の updateTweet()メソッドに渡します

## 動作確認

- アプリケーションを起動し、ターミナルから以下のコマンドを実行します

```shell
# put request
$ curl -X PUT -H 'Content-Type:application/json' -d '{"name":"更新後の名前", "message":"更新後のメッセージ"}' localhost:8080/tweet/1

# response
{"id":1,"name":"更新後の名前","message":"更新後のメッセージ"}
```

- データベースを確認し、データが更新されていることを確認します

```
mysql> select * from tweet;
+----+-----------------------------+--------------------+
| id | message                     | name               |
+----+-----------------------------+--------------------+
|  1 | 更新後のメッセージ          | 更新後の名前       |
+----+-----------------------------+--------------------+
1 row in set (0.01 sec)
```

- 最後に、データの削除を確認するため、ターミナルから以下のコマンドを実行します

```shell
# delete request
curl -X DELETE -H 'Content-Type:application/json' localhost:8080/tweet/1

# responseは無し
```

- データベースを確認し、データが削除されていれば OK です！

```
mysql> select * from tweet;
Empty set (0.00 sec)
```

# 参考

- [はじめての Spring Boot―「Spring Framework」で簡単 Java アプリ開発 (I・O BOOKS)](https://www.google.com/aclk?sa=L&ai=DChcSEwi-55Swm6XkAhWIq5YKHWZlA1AYABAJGgJ0bA&sig=AOD64_0Pw9z3Nr6_eStsZmhBoO8EM9PcNg&ctype=5&q=&ved=0ahUKEwjymo-wm6XkAhVsHKYKHUhJC40Q9A4I0gE&adurl=)
- [Spring boot/Hibernate fails creating table
](https://stackoverflow.com/questions/52574685/spring-boot-hibernate-fails-creating-table)