Spring boot + JPA + Flyway on herokuでLINE Botを作る

## 概要
Spring BootとJPAとFlywayとherokuを使って、DBから返信メッセージを取得するLINE Botを作る。
あくまでちょっと動かしてみた程度のソースなので、細かいところのツッコミはご容赦ください。
また以下については説明しないので、リンク先を参照してください。

- **LINE Channelの作成**
    - https://developers.line.biz/ja/docs/messaging-api/getting-started/
- **herokuでアプリを作成**
    - https://signup.heroku.com/
- **JPA (Java Persistence API)** => Java標準のORM
    - https://www.vogella.com/tutorials/JavaPersistenceAPI/article.html
- **Flyway** => Migrationツール
    - https://flywaydb.org/

## 関連
- [line-bot-sdk-javaを使ってSpring bootでLINE Botを作る](https://qiita.com/Esfahan/items/25d74247778c87454634)
- [LINE BOT APIのreplyでたまにInvalid reply tokenが発生する](https://qiita.com/Esfahan/items/949ac0240dddd128b662)

## 前提
lineが配布しているサンプルコードを使う。
https://github.com/line/line-bot-sdk-java.git

## 環境
- Java 11.02
- Spring Boot 2.4.9 (line-bot-sdk-javaで使われているversion)

## 要件
LINEでbot宛にメッセージを送信すると、その送信文字列に応じた返信メッセージをDBのTableから抽出し、返信するシステムを作る。返信の条件は以下の通り。

| 送信Message | 返信Message |
| --- | --- |
| ハロー | ハローワールド |
| hello | Hello world!! |
| それ以外 | 返信しない |

### アーキテクチャ
- heroku
- PostgreSQL 13.3 (on heroku)

### ディレクトリ構成
サンプルコードの中で、今回編集、新規追加するファイルだけを以下に記した。

```console
.
├ ─ ─  Procfile
└ ─ ─  sample-spring-boot-kitchensink
     ├ ─ ─  build.gradle
     └ ─ ─  src
         └ ─ ─  main
             ├ ─ ─  java
             │    └ ─ ─  com
             │        └ ─ ─  example
             │            └ ─ ─  bot
             │                └ ─ ─  spring
             │                    ├ ─ ─  KitchenSinkController.java
             │                    ├ ─ ─  models
             │                    │    └ ─ ─  TextMessageModel.java
             │                    ├ ─ ─  repositories
             │                    │    └ ─ ─  TextMessageRepository.java
             │                    └ ─ ─  services
             │                         └ ─ ─  TextMessageService.java
             └ ─ ─  resources
                 ├ ─ ─  application.yml
                 └ ─ ─  db
                     └ ─ ─  migration
                         └ ─ ─  V1_0__create_table_text_messages.sql
```

サンプルコードの中のsample-spring-boot-kitchensinkを利用するので、以下３つのディレクトリは消してしまってもOK。

- sample-manage-audience
- sample-spring-boot-echo
- sample-spring-boot-echo-kotlin

### DB Table
text_messagesという返信用メッセージを入れるテーブルを作成する。Flywayでmigrateするので、手動での作成は不要。

```sql
line-bot-sample::DATABASE=> \d text_messages;
                                    Table "public.text_messages"
 Column  |          Type          | Collation | Nullable |                  Default
---------+------------------------+-----------+----------+-------------------------------------------
 id      | integer                |           | not null | nextval('text_messages_id_seq'::regclass)
 message | character varying(255) |           | not null |
Indexes:
    "text_messages_pkey" PRIMARY KEY, btree (id)
```

### GitHUB
完成品はこちら
https://github.com/Esfahan/line-bot-java-sample

## 作成手順
### サンプルコードの取得
lineが配布しているサンプルコードを使う。

```
$ git clone https://github.com/line/line-bot-sdk-java.git
```

### JPAとflywayを利用できるようにする
sample-spring-boot-kitchensink/build.gradleに以下を追記する。

```diff:sample-spring-boot-kitchensink/build.gradle
dependencies {
     implementation project(':line-bot-spring-boot')
     implementation "org.springframework.boot:spring-boot-starter-web"
     implementation 'com.google.guava:guava'
+    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
+    implementation 'org.flywaydb:flyway-core'
+    runtimeOnly 'org.postgresql:postgresql'
 }
```

### application.ymlを作成
sample-spring-boot-kitchensink/src/main/resources/application.ymlを新規作成する。
環境変数の値については[後述](#herokuの環境変数設定)する。

```yaml
line.bot:
  channel-token: ${CHANNEL_TOKEN}
  channel-secret: ${CHANNEL_SECRET}
  handler.path: /callback

spring:
  jpa:
    database: POSTGRESQL
    open-in-view: True
  datasource:
    url: ${DATABASE_JDBC_URL}
    username: ${DATABASE_USER}
    password: ${DATABASE_PASSWORD}
    driver-class-name: org.postgresql.Driver
    connectionProperties: useUnicode=true;characterEncoding=utf-8;
  flyway:
    locations: classpath:/db/migration
    baseline-on-migrate: true
```

その他のプロパティは以下を参照。
https://spring.pleiades.io/spring-boot/docs/current/reference/html/application-properties.html

### Migrationファイルを作成
マイグレーションファイルの命名規則は以下を参照。
https://garafu.blogspot.com/2020/06/how-to-use-flyway.html


今回は以下の名前で作成する。
V1_0__create_table_text_messages.sql

application.ymlのflyway.locationsで定義したように、resources/db/migration/にsqlファイルを設置するとmigrationが実行されるので、ディレクトリを作ってsqlファイルを作成する。

```console
$ mkdir -p sample-spring-boot-kitchensink/src/main/resources/db/migration/
$ vi sample-spring-boot-kitchensink/src/main/resources/db/migration/V1_0__create_table_text_messages.sql
```

```sql:V1_0__create_table_text_messages.sql
CREATE TABLE text_messages(
  id serial PRIMARY KEY,
  message varchar(255) NOT NULL
);
```

### ディレクトリ作成
各ソースコードを設置するmodels/,repositories/,services/ディレクトリを作成する。

```console
$ mkdir sample-spring-boot-kitchensink/src/main/java/com/example/bot/spring/{models,repositories,services}
```

### Modelの作成
以下のファイルを新規作成する。
sample-spring-boot-kitchensink/src/main/java/com/example/bot/spring/models/TextMessageModel.java

```java:TextMessageModel.java
package com.example.bot.spring.models;

import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;

@Entity
@Table(name="text_messages")
public class TextMessageModel {
    @Id
    private Integer id;

    public String message;
}
```

### Repositoryの作成
以下のファイルを新規作成する。
sample-spring-boot-kitchensink/src/main/java/com/example/bot/spring/repositories/TextMessageRepository.java

```java:TextMessageRepository.java
package com.example.bot.spring.repositories;

import com.example.bot.spring.models.TextMessageModel;
import org.springframework.data.jpa.repository.JpaRepository;

public interface TextMessageRepository extends JpaRepository<TextMessageModel, Integer> {
}
```

### Serviceの作成
以下のファイルを新規作成する。
sample-spring-boot-kitchensink/src/main/java/com/example/bot/spring/services/TextMessageService.java

```java:TextMessageService.java
package com.example.bot.spring.services;

import com.example.bot.spring.repositories.TextMessageRepository;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Slf4j
@Service
public class TextMessageService {
    private String replyTextMessage;
    private boolean replyFlg;

    @Autowired
    TextMessageRepository textMessageRepository;

    public void fetchReplyTextMessage (String message) {
        log.info("fetchReplyTextMessage is called: {}", message);

        Integer id = replyId(message);
        if (id > 0) {
            this.replyTextMessage = textMessageRepository.findById(id).get().message;
            this.replyFlg = true;
        } else {
            this.replyFlg = false;
        }
    }

    public String getReplyMessage() {
        return this.replyTextMessage;
    }

    public boolean getReplyFlg() {
        return this.replyFlg;
    }

    private Integer replyId(String message) {
        switch (message) {
            case "ハロー": {
                return 1;
            }
            case "hello": {
                return 2;
            }
            default: {
                return 0;
            }
        }
    }
}
```

### controller
以下のファイルを編集する。
sample-spring-boot-kitchensink/src/main/java/com/example/bot/spring/KitchenSinkController.java

ここでは、処理内容をシンプルに分かりやすくするため、handleTextMessageEventメソッド１つで全ての処理するように書き換える。

```diff:KitchenSinkController.java
+import com.example.bot.spring.services.TextMessageService;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.web.servlet.support.ServletUriComponentsBuilder;

public class KitchenSinkController {
     @Autowired
     private LineBlobClient lineBlobClient;

+    @Autowired
+    private TextMessageService textMessageService;
+
     @EventMapping
     public void handleTextMessageEvent(MessageEvent<TextMessageContent> event) throws Exception {
         TextMessageContent message = event.getMessage();
-        handleTextContent(event.getReplyToken(), event, message);
+
+        log.info("TextMessageContent is called");
+
+        textMessageService.fetchReplyTextMessage(message.getText());
+
+        if (textMessageService.getReplyFlg()) {
+            log.info("replyFlg is true");
+
+            String replyText = textMessageService.getReplyMessage();
+            List<Message> replyMessage = singletonList(new TextMessage(replyText));
+            String replyToken = event.getReplyToken();
+
+            log.info("Returns echo message {}: {}", replyToken, replyText);
+
+            boolean notificationDisabled = false;
+            try {
+                BotApiResponse apiResponse = lineMessagingClient
+                        .replyMessage(new ReplyMessage(replyToken, replyMessage, notificationDisabled))
+                        .get();
+                log.info("Sent messages: {}", apiResponse);
+            } catch (InterruptedException | ExecutionException e) {
+                throw new RuntimeException(e);
+            }
+        }
     }
```

### Procfileを書き換え
sample-spring-boot-kitchensinkを利用するため、Procfileを以下の様に書き換える。

```diff
-web: java $JAVA_OPTS -jar sample-spring-boot-echo/build/libs/sample-spring-boot-echo-*.jar --server.port=$PORT
+web: java $JAVA_OPTS -jar sample-spring-boot-kitchensink/build/libs/sample-spring-boot-kitchensink-*.jar --server.port=$PORT
```

### .gitignore
`*.jar`をignoreすると./gradle/wrapper/gradle-wrapper.jarがDeployされず、以下のエラーが出るので編集する。
`Error: Could not find or load main class org.gradle.wrapper.GradleWrapperMain`

```diff:.gitignore
# Package Files #
-*.jar
+#*.jar
 *.war
 *.ear
```

### herokuの環境変数設定
herokuに以下の環境変数を設定する。

| Key | Value |
| --- | --- |
| CHANNEL_SECRET | LINEのChannel access token |
| CHANNEL_TOKEN | LINEのChannel secret |
| DATABSE_JDBC_URI | jdbc:postgresql://herokuのpostgresのhostname:5432/dbname |
| DATABSE_USER | herokuのpostgresのuser name |
| DATABSE_PASSWORD | herokuのpostgresのpassword |

## Deploy
herokuにdeployする。

```console
$ git remote add heroku https://git.heroku.com/${HEROKU_APP_NAME}.git
$ git add --all
$ git commit -m 'Added a sample codeset for Qiita'
$ git push heroku master
```

## Migrationの結果を確認
herokuにdeployすると、postgresに以下のテーブルが作成される。
Intellijなどローカルの場合は、IDEでプログラムを実行すれば接続先のDBにテーブルが作成される。

```console
$ heroku pg:psql postgresql-abcdefg-12345 --app line-bot-sample
```

```sql
line-bot-sample::DATABASE=> \d
                     List of relations
 Schema |         Name          |   Type   |     Owner
--------+-----------------------+----------+----------------
 public | flyway_schema_history | table    | sample-user
 public | text_messages         | table    | sample-user
 public | text_messages_id_seq  | sequence | sample-user
(3 rows)
```

## Insert
DBに以下をinsertする。これが、返信用メッセージとなる。

```sql
line-bot-sample::DATABASE=> insert into text_messages (message) values ('ハローワールド');
INSERT 0 1
line-bot-sample::DATABASE=> insert into text_messages (message) values ('Hello world!!');
INSERT 0 1
line-bot-sample::DATABASE=> select * from text_messages;
 id |    message
----+----------------
  1 | ハローワールド
  2 | Hello world!!
(2 rows)
```

## LINEでWebhook URLを設定
[LINE Developer](https://developers.line.biz/)のMessage APIのWebhook Settingsに、herokuのURLを登録する。

Webhook URL: https://{HEROKU_APP_NAME}.herokuapp.com/callback
Use webhook: onにする

これでbotにメッセージを送信すると、以下のような結果となる。
![IMG_5259.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/105936/82f4287e-7727-2f49-a4c4-facb0a75682c.png)

## テストコードを書く場合
以下にtest用ディレクトリを作成し、その下にテストコードを作成する。

```console
$ mkdir -p sample-spring-boot-kitchensink/src/test/java/com/example/bot/spring/
```

## 参考
https://tosi-tech.net/2019/04/flyway-with-spring-boot/
https://tomokazu-kozuma.com/how-to-migrate-mysql-using-flyway/

## 関連
- [LINE BOT APIのreplyでたまにInvalid reply tokenが発生する](https://qiita.com/Esfahan/items/949ac0240dddd128b662)