GradleでFlywayでMySQLをマイグレーション on Docker

# はじめに
昔Flyway使ったDBマイグレーションのことはじめを作ったけど（[これ](https://qiita.com/kazzool/items/ab85d1f67813b9d438ff)）、ローカルの環境準備とかが色々面倒なので、
DB開発関連を全部ひとまとめにして開発環境シェアも簡単にしましょうや、というお話。

# 経緯
暇つぶし。

# 環境とか
- Windows 10 Home
- Docker version 20.10.5
- docker-compose version 1.29.0
- Visual Studio Code

# 作りたいもののイメージ
こんなかんじ。
![mysql-development-skeleton.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/584842/2ee721d2-c951-b72b-daaf-b35859c74494.png)

- Docker Compose
    - ローカルに依存しない環境配布をするため全部コンテナ化
- Migrate Service
    - Dockerコンテナ内でGradleプロジェクトとして動かす
        - CIする時のことを考慮してDockerイメージを…とか色々考えたけど、  
          あらゆる環境パターンを考慮するとか面倒臭すぎてあきらめ…
    - flywayのdockerイメージもあったけど、csvインポートでInsert文生成とかも考えてGradle経由に
        - これ結局開発過程でJava書くやんけ…って
- Database Service
    - MySQLの初期セットアップも省けてうれしい

# やってみる
## 1. まずはDatabase Service(MySQLのコンテナ)を作る
必要なファイルを作成する。

```docker-compose.yml
version: '3.8'

services:
  database:
    image: mysql:8.0
    container_name: mysql
    environment:
        MYSQL_DATABASE: sample_db
        MYSQL_ROOT_PASSWORD: root
    ports:
        - "3306:3306"
    volumes:
        - ./docker/database/conf.d:/etc/mysql/conf.d
        - ./docker/database/initdb.d:/docker-entrypoint-initdb.d
        - mysql-data:/var/lib/mysql # ボリュームマウント(データ永続化)の方法については諸説あるらしい
volumes: 
  mysql-data:version: '3.8'
```

```conf:docker/mysql/conf.d/my.conf
# 設定値系はお好みでカスタマイズ
[mysqld]
character-set-server=utf8mb4
collation-server=utf8mb4_general_ci
explicit-defaults-for-timestamp=1
general-log=1
general-log-file=/var/log/mysql/mysqld.log

[client]
default-character-set=utf8mb4
```

```sql:docker/mysql/initdb.d/init.sql
-- とりあえずflywayから接続する用のユーザとその権限はここで作っておく
-- 実際のシステムユーザなどはmigrateファイルで管理していくのが良さそう
CREATE USER 'flyway'@'%' IDENTIFIED BY 'flyway';
GRANT ALL ON sample_db.* TO 'flyway'@'%';
FLUSH PRIVILEGES;
```

用意した定義でコンテナ起動。

```
docker-compose up -d
```

MySQLが立ったらコンテナ内とかクライアントから接続確認などして完了。

## 2. Migrate Service(Gradle＋Flywayのコンテナ)を作る
docker-compose.ymlに追記。
openjdkのイメージ上にローカルのGradleプロジェクトをマウントし、mysqlコンテナに繋げるなどする。

```diff:docker-compose.yml
version: '3.8'

services:
    database:
        image: mysql:8.0
        container_name: mysql
        environment:
            MYSQL_DATABASE: sample_db
            MYSQL_ROOT_PASSWORD: root
        ports:
            - "3306:3306"
        volumes:
            - ./docker/database/conf.d:/etc/mysql/conf.d
            - ./docker/database/initdb.d:/docker-entrypoint-initdb.d
            - mysql-data:/var/lib/mysql
+   migrate:
+       image: openjdk:11
+       container_name: gradle-flyway
+       volumes:
+           - ./docker/migrate:/migrate:cached
+       working_dir: /migrate
+       tty: true
+       depends_on:
+           - database
volumes: 
    mysql-data: 
```

上記でマウント指定したディレクトリに、[当時を思い出しながら](https://qiita.com/kazzool/items/ab85d1f67813b9d438ff)、Flyway実行用のGradleプロジェクトを作る。
ちなみに今回はMigrateコンテナ->MySQLコンテナという接続を行うため、flyway.urlで指定するDBのホスト名はlocalhostではなく対象コンテナのService名にする必要があった（今回だと`database`）。

docker-composeの変更を反映させたら、起動したmigrateコンテナに入る。

```
docker-compose exec migrate bash
```

とりあえず以下とかでflywayタスクが通ってマイグレーションステータスとかが見れればOK。

```
./gradlew flywayInfo
```

## 3. せっかくなのでJavaからSQL生成するサンプルも作る
前回Javaベースでのマイグレーションはやってなかったので、いろいろ考慮不足も発見。
Java実装時に`org.flywaydb`がimportできねぇとか言われて依存関係を修正したりなど。

```diff:docker/migrate/build.gradle
- buildscript {
-     repositories {
-         mavenCentral()
-     }
-     dependencies {
-         classpath 'org.flywaydb:flyway-gradle-plugin:7.12.1'
-         classpath 'mysql:mysql-connector-java:8.0.15'
-     }
- }
- 
  plugins {
      id "java"
      id "org.flywaydb.flyway" version "7.12.1"
  }

  repositories {
      mavenCentral()
  }

  dependencies {
      implementation "org.flywaydb:flyway-core:7.12.1"
      runtimeOnly 'mysql:mysql-connector-java:8.0.15'
  }

+ dependencies {
+     implementation "org.flywaydb:flyway-core:7.12.1"
+     runtimeOnly 'mysql:mysql-connector-java:8.0.15'
+ }
+ 
  flyway {
      url = getProperty("${env()}.flyway.url") + "?&allowPublicKeyRetrieval=true&useSSL=false"
      user = getProperty("${env()}.flyway.user")
      password = getProperty("${env()}.flyway.password")
      baselineVersion = "00.000.000"
+     locations = ['classpath:db/migration']
  }

  def env() {
      project.hasProperty('env') ? project.properties['env'] : 'local'
  }
```

CSVインポートしてInsert文作るJavaをとりあえず実装。
skeletonプロジェクトとして活用するにはcsv加工（ヘッダー有無、セパレータ指定、columnのクォート除去）とかのUtilクラス化が必要だなーと思いつつ、力尽きてFinish。

```java:docker/migrate/src/main/java/db/migration/V00_000_003__Insert_UserTable.java
public class V00_000_003__Insert_UserTable extends BaseJavaMigration {

    private static final String PATH = "src/main/resources/db/migration/seed/V00_000_003__Insert_UserTable.csv";
    private static final String DB_NAME = "sample_db";
    private static final String TABLE_NAME = "t_user";
    private static final String INSERT_USER = "flyway";
    private static final String SEPARATOR = ",";

    public void migrate(Context context) throws Exception {

        Path path = Paths.get(PATH);

        try (BufferedReader reader = Files.newBufferedReader(path)) {

            String line;

            while ((line = reader.readLine()) != null) {

                String[] columns = line.split(SEPARATOR);

                String userId = columns[0];
                String displayName = columns[1];

                StringBuilder sql = new StringBuilder();
                sql.append("INSERT ");
                sql.append("INTO " + DB_NAME + "." + TABLE_NAME + " ");
                sql.append("VALUES ( ");
                sql.append("    ?");
                sql.append("  , ?");
                sql.append("  , current_timestamp");
                sql.append("  , '" + INSERT_USER + "'");
                sql.append("  , current_timestamp");
                sql.append("  , '" + INSERT_USER + "'");
                sql.append(");");

                try (PreparedStatement pstmt = context.getConnection().prepareStatement(sql.toString())) {
                    pstmt.setString(1, userId);
                    pstmt.setString(2, displayName);
                    pstmt.execute();
                }
            }
        }
    }
}
```

```csv:docker/migrate/src/main/resources/db/migration/seed/V00_000_003__Insert_UserTable.csv 
000000000000000000000000000001,test_user_1
000000000000000000000000000002,test_user_2
000000000000000000000000000003,test_user_3
000000000000000000000000000004,test_user_4
000000000000000000000000000005,test_user_5
000000000000000000000000000006,test_user_6
000000000000000000000000000007,test_user_7
000000000000000000000000000008,test_user_8
000000000000000000000000000009,test_user_9
000000000000000000000000000010,test_user_10 
```

最後に

```
./gradlew build
./gradlew flywayMigrate
```

して、無事Insertされておしまい。

# おわりに
後日READMEに書いたとおりにサクッと環境複製できるかMacで試してみます。
あとFlywayで開発・運用ってバージョンの競合とかどうすんだろ？って思って調べたらバージョン名をタイムスタンプにするって解決策があるんですね。
チケット番号（Redmineとかの）使えば要件・案件のトラッキングもいい感じになんじゃね？とか悶々と考えてたりします。

# 成果物
https://github.com/kazzool/mysql-development-skeleton

# 参考
- [初心者向けdocker-composeコマンド逆引き](https://qiita.com/okyk/items/a374ddb3f853d1688820)
- [docker-composeでMySQLを起動してみた](https://qiita.com/forests-k/items/e8e4272f30ca2fdec6ac)
- [Flyway + MySQL + Docker でマイグレーション環境構築](https://qiita.com/oskamathis/items/04069bc74d9e664705dd)
- [Spring Boot + Flywayでコードによるデータベースマイグレーション](https://qiita.com/mako_wis/items/3f447a3636774cad8e30)
- [(公式)Java-based migrations](https://flywaydb.org/documentation/concepts/migrations#java-based-migrations)