DBマイグレーションツール活用のすすめ〜Flyway〜





# 概要
巷にはDBマイグレーションツールなるものがあります。この、マイグレーションというのは移行を意味しますが、いわゆる旧環境から新環境への移行といったデータ移行を意味するものではないのです。DB環境(DBスキーマ+データ)を移行して、同一状態のDB環境を手軽に構築できるようするツールなのです。


ここでは[Flyway](http://flywaydb.org/)というDBマイグレーションツールを試してみます。DB環境をバージョン管理して、DB移行を簡単にしてくれるツールです。

開発時、複数の開発者がそれぞれのDBを使うことがあります。それらのDB環境を一致させることが簡単になるので、古いDB環境を使って間違った実装・テストをするといった事態を未然に防止できるようになります。

ソースコードのバージョン管理が開発を支えるように、DB環境のバージョン管理も開発を支えてくれるはず。今後、システム開発の現場でDBマイグレーションツールの利用を検討する機会になれば嬉しい、という思いを込めて、メモしておきます。(少なくとも自分自身の備忘録にはなるはず)



# Flyway + H2 + Gradle で試す

手軽にFlywayを試してみて、そのパワーを感じてみます。DBにH2を使い、gradle経由でFlywayを使います。通常開発でFlyway単発で使うことは少ないでしょうから。


## 環境
* OS-X (ここでは。WindowsでもCentOSでもいける。）
* Java（最新バージョン入れといたら間違いない。）
* H2（もちろんMySQLとかでもよい）


## 準備
このメモで完結できるよう、インストールについてもここに書いておきます。
インストールと言ってもアーカイブを展開するだけ。


1. H2のインストール
2. Gradleのインストール

### 1.H2のインストール
[H2](http://www.h2database.com/html/main.html)はJavaでできたRDBエンジン。HPより最新版をダウンロードする。2015/12/2時点の最新バージョンは1.4.190。
適当なフォルダに展開して、`h2/bin/h2.sh` を実行すると、ブラウザが起動し、H2 Console が開く。

> なおOS-X環境だと、上記ファイルを実行すると正しく動かない。
>
> ```
> $ ./h2.sh
> zsh: ./h2.sh: bad interpreter: /bin/sh^M: no such file or directory
> ```
>
> 改行コードがDOS(CR+LF)になっているから。
> UNIX(LF)にしてやるとうまくいく。
>
> ```
> $ cp h2.sh h2.sh.org
> $ tr -d ¥¥r < h2.sh.org > h2.sh
> $ chmod +x h2.sh
> ```


![スクリーンショット 2015-12-01 1.27.54.png](https://qiita-image-store.s3.amazonaws.com/0/79341/73615368-0df1-bce6-63ec-e1c55bc1a788.png "スクリーンショット 2015-12-01 1.27.54.png")





### 2.Gradleのインストール
[gradle](http://gradle.org/gradle-download/)のHPからアーカイブをダウンロード。Binary only distributionでよい。2012/12/1現在の最新バージョンは2.9。
それを適当なフォルダに展開する。

> 個人的にはsdkmanを使ったやり方で入れてる。
> OS-Xであれば`brew`でインストールするのが簡単かも。

ここまでで準備は完了。



# Flywayを扱えるプロジェクトを作る

## Gradleでプロジェクトを作る
まず、`gradle init`を実行してプロジェクトを作る。

```
$ gradle init --type java-library
Starting a new Gradle Daemon for this build (subsequent builds will be faster).
:wrapper
:init

BUILD SUCCESSFUL

Total time: 7.385 secs
```


こんな感じでフォルダが生成される。

```bash
$ du
176	./.gradle/2.9/taskArtifacts
176	./.gradle/2.9
176	./.gradle
120	./gradle/wrapper
120	./gradle
8	./src/main/java
8	./src/main
8	./src/test/java
8	./src/test
16	./src
352	.
```




## build.gradleファイルにflyway関連ライブラリを組み込む
カレントディレクトリに、__build.gradle__というファイルができる。これを修正してflywayライブラリを組み込む。
修正後の__build.gradle__は以下のような感じ。

> mavenCnetral()でなくjcenter()でもいいはず。
> 生成されたコメントは消してます。

```build.gradle
buildscript {
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath 'com.h2database:h2:1.4.190'
        classpath 'org.flywaydb:flyway-gradle-plugin:3.2.1'
    }
}

apply plugin: 'java'
apply plugin: 'org.flywaydb.flyway'

repositories {
    mavenCentral()
}

dependencies {
    compile 'org.slf4j:slf4j-api:1.7.13' // init時のもの
    testCompile 'junit:junit:4.12' // init時のもの
}

flyway {
    user = 'sa'
    //url = 'jdbc:h2:./dummy' // Embedded
    url = 'jdbc:h2:tcp://localhost/~/dummy' // c/s
}
```

うまく設定できているかを確認してみる。`gradle tasks`を実行して、flywayタスクがあればとりあえずOK。

```
flyway tasks
------------
flywayBaseline - Baselines an existing database, excluding all migrations up to and including baselineVersion.
flywayClean - Drops all objects in the configured schemas.
flywayInfo - Prints the details and status information about all the migrations.
flywayInit - Baselines an existing database, excluding all migrations up to and including baselineVersion.
flywayMigrate - Migrates the schema to the latest version.
flywayRepair - Repairs the Flyway metadata table.
flywayValidate - Validates the applied migrations against the ones available on the classpath.

```




# Flywayを試す

## マイグレーション用のSQLファイルを追加する

まずmigrationフォルダを作る。

```bash
$ mkdir -p src/main/resources/db/migration
```

次に初期DBを作成するSQLスクリプトを作る。先ほどのmigrationフォルダに、V1__Initial_DB.sqlというファイルを作成する。中身はこんな感じにする。

```sql:V1__Initail_DB.sql
CREATE TABLE USER (
 ID LONG NOT NULL IDENTITY,
 USER_ID VARCHAR(10) NOT NULL,
 PASSWORD VARCHAR(20) NOT NULL,
 NAME VARCHAR(100) NOT NULL
);
```

> マイグレーションファイルは命名規則が決まっている。
> V1__説明.sql というフォーマット。
> 最初にプレフィックスが__V__　。これがデフォルト。変更できる。
> 次にバージョン番号。数値。1.1や1_2_3などとかける。
> 次が区切り文字。__ これは固定。
> 次が説明文。日本語OK。
> 最後にサフィックス。デフォルトは__.sql__。変更できる。

## マイグレーション状況を確認する

`gradle flywayInfo` と入力し、DBの状態を確認する。
先ほど作成したマイグレーションファイルの情報が表示される。

```bash
$ gradle flywayInfo
:compileJava UP-TO-DATE
:processResources UP-TO-DATE
:classes UP-TO-DATE
:compileTestJava UP-TO-DATE
:processTestResources UP-TO-DATE
:testClasses UP-TO-DATE
:flywayInfo
+---------+-------------+---------------------+---------+
| Version | Description | Installed on        | State   |
+---------+-------------+---------------------+---------+
| 1       | Initial DB  |                     | Pending |
+---------+-------------+---------------------+---------+


BUILD SUCCESSFUL

Total time: 0.516 secs
```

H2 Consoleを確認。特にDBは変化なし。

![スクリーンショット 2015-12-01 1.36.07.png](https://qiita-image-store.s3.amazonaws.com/0/79341/21ee5b8d-acce-4987-efc7-e1047abf7ca3.png "スクリーンショット 2015-12-01 1.36.07.png")




## マイグレーションする
`gradle flywayMigrate` と入力しマイグレーショする。

```bash
$ gradle flywayMigrate
:compileJava UP-TO-DATE
:processResources UP-TO-DATE
:classes UP-TO-DATE
:compileTestJava UP-TO-DATE
:processTestResources UP-TO-DATE
:testClasses UP-TO-DATE
:flywayMigrate

BUILD SUCCESSFUL

Total time: 0.531 secs
```

`gradle flywayInfo` で確認。

```bash
$ gradle flywayInfo
:compileJava UP-TO-DATE
:processResources UP-TO-DATE
:classes UP-TO-DATE
:compileTestJava UP-TO-DATE
:processTestResources UP-TO-DATE
:testClasses UP-TO-DATE
:flywayInfo
+---------+-------------+---------------------+---------+
| Version | Description | Installed on        | State   |
+---------+-------------+---------------------+---------+
| 1       | Initial DB  | 2015-12-01 01:39:15 | Success |
+---------+-------------+---------------------+---------+


BUILD SUCCESSFUL

Total time: 0.523 secs
```

StateがPengingからSuceessになる。

H2 ConsocleでDBを確認する。USERテーブルとschema_versionテーブルができている。
![スクリーンショット 2015-12-01 1.41.18.png](https://qiita-image-store.s3.amazonaws.com/0/79341/980c935d-733a-f96d-a7aa-2b5a51f5754f.png "スクリーンショット 2015-12-01 1.41.18.png")

なお、`schema_versionテーブル`はこんな感じ。
![スクリーンショット 2015-12-01 1.43.01.png](https://qiita-image-store.s3.amazonaws.com/0/79341/50212a60-947e-4f13-47a0-0f9ad4692646.png "スクリーンショット 2015-12-01 1.43.01.png")




## さらにマイグレーションする
次のバージョン、V1.1__User_Data.sql を追加してみる。

```sql:V1.1_User_Data.sql
INSERT INTO USER (USER_ID, PASSWORD, NAME) VALUES ('TEST01', 'TEST', 'テストユーザー01');
INSERT INTO USER (USER_ID, PASSWORD, NAME) VALUES ('TEST02', 'TEST', 'テストユーザー02');
INSERT INTO USER (USER_ID, PASSWORD, NAME) VALUES ('TEST03', 'TEST', 'テストユーザー03');
INSERT INTO USER (USER_ID, PASSWORD, NAME) VALUES ('TEST04', 'TEST', 'テストユーザー04');
INSERT INTO USER (USER_ID, PASSWORD, NAME) VALUES ('TEST05', 'TEST', 'テストユーザー05');
```

`gradle flywayInfo` で確認。

```bash
$ gradle flywayInfo
:compileJava UP-TO-DATE
:processResources
:classes
:compileTestJava
:processTestResources UP-TO-DATE
:testClasses
:flywayInfo
+---------+-------------+---------------------+---------+
| Version | Description | Installed on        | State   |
+---------+-------------+---------------------+---------+
| 1       | Initial DB  | 2015-12-01 01:39:15 | Success |
| 1.1     | User Data   |                     | Pending |
+---------+-------------+---------------------+---------+


BUILD SUCCESSFUL

Total time: 0.537 secs
```

Version 1.1 がPenging状態。
`gradle flywayMigrate` して、`gradle flywayInfo` する。

```bash
$ gradle flywayMigrate
:compileJava UP-TO-DATE
:processResources UP-TO-DATE
:classes UP-TO-DATE
:compileTestJava UP-TO-DATE
:processTestResources UP-TO-DATE
:testClasses UP-TO-DATE
:flywayMigrate

BUILD SUCCESSFUL

Total time: 0.515 secs
```

```bash
$ gradle flywayInfo
:compileJava UP-TO-DATE
:processResources UP-TO-DATE
:classes UP-TO-DATE
:compileTestJava UP-TO-DATE
:processTestResources UP-TO-DATE
:testClasses UP-TO-DATE
:flywayInfo
+---------+-------------+---------------------+---------+
| Version | Description | Installed on        | State   |
+---------+-------------+---------------------+---------+
| 1       | Initial DB  | 2015-12-01 02:06:10 | Success |
| 1.1     | User Data   | 2015-12-01 02:07:04 | Success |
+---------+-------------+---------------------+---------+


BUILD SUCCESSFUL

Total time: 0.513 secs
```

DBを見てみる。
![スクリーンショット 2015-12-01 2.09.21.png](https://qiita-image-store.s3.amazonaws.com/0/79341/cddb3f2a-85f0-6c47-3796-0631d65ac826.png "スクリーンショット 2015-12-01 2.09.21.png")

こんな感じでDB環境のバージョン管理をしていく。





# その他
## マイグレーション失敗時の対処方法
マイグレーションファイルにミスがあると当然のことながらマイグレーションに失敗する。
たとえば、こんなマイグレーションファイルを用意する。

```V1.2__Modify_User.sql
ALTER TABLE USER ADD COLUMN UPDATE_DATE DATA; // 本当はDATE
```

`gralde flywayMigrate`としてみる。すると、、、

```bash
$ gradle flywayMigrate
:compileJava UP-TO-DATE
:processResources UP-TO-DATE
:classes UP-TO-DATE
:compileTestJava UP-TO-DATE
:processTestResources UP-TO-DATE
:testClasses UP-TO-DATE
:flywayMigrate
Migration of schema "PUBLIC" to version 1.2 failed! Please restore backups and roll back database and code!
:flywayMigrate FAILED

FAILURE: Build failed with an exception.

* What went wrong:
Execution failed for task ':flywayMigrate'.
> Error occurred while executing flywayMigrate

  Migration V1.2__Modify_User.sql failed
  --------------------------------------
  SQL State  : HY004
  Error Code : 50004
  Message    : 不明なデータ型: "DATA"
  Unknown data type: "DATA"; SQL statement:
  ALTER TABLE USER ADD COLUMN UPDATE_DATE DATA [50004-190]
  Location   : db/migration/V1.2__Modify_User.sql (/Users/hagi/tmp/flyway/build/resources/main/db/migration/V1.2__Modify_User.sql)
  Line       : 1
  Statement  : ALTER TABLE USER ADD COLUMN UPDATE_DATE DATA

  不明なデータ型: "DATA"
  Unknown data type: "DATA"; SQL statement:
  ALTER TABLE USER ADD COLUMN UPDATE_DATE DATA [50004-190]

* Try:
Run with --stacktrace option to get the stack trace. Run with --info or --debug option to get more log output.

BUILD FAILED

Total time: 0.555 secs
```

`gradle flywayInfo`とすると、こんな感じになる。

```bash
$ gradle flywayInfo                                                                                                       x [火 1 22:49:07]
:compileJava UP-TO-DATE
:processResources UP-TO-DATE
:classes UP-TO-DATE
:compileTestJava UP-TO-DATE
:processTestResources UP-TO-DATE
:testClasses UP-TO-DATE
:flywayInfo
+---------+-------------+---------------------+---------+
| Version | Description | Installed on        | State   |
+---------+-------------+---------------------+---------+
| 1       | Initial DB  | 2015-12-01 22:46:53 | Success |
| 1.1     | User Data   | 2015-12-01 22:46:53 | Success |
| 1.2     | Modify User | 2015-12-01 22:49:07 | Failed  |
+---------+-------------+---------------------+---------+


BUILD SUCCESSFUL

Total time: 0.552 secs
```

Version1.2のマイグレーションのStateがFailedとなる。
こうなったら、`gradle flywayRepair`としてPendingに戻し、マイグレーションファイルを修正し、再度`gradle flywayMigrate`してやる。



## マイグレーションファイルのバリデーションについて
マイグレーション済みのV1.1__User_Data.sqlを変更して、`gradle flywayValidate`をすると、こんな感じでやっちまったなぁ、となる。
DBに適用済みのバージョンとローカルにあるマイグレーションファイルが異なることを教えてくれる。

```bash
$ gradle flywayValidate
:compileJava UP-TO-DATE
:processResources
:classes
:compileTestJava
:processTestResources UP-TO-DATE
:testClasses
:flywayValidate FAILED

FAILURE: Build failed with an exception.

* What went wrong:
Execution failed for task ':flywayValidate'.
> Error occurred while executing flywayValidate
  Validate failed. Migration Checksum mismatch for migration 1.1
  -> Applied to database : -1549820110
  -> Resolved locally    : -702322096

* Try:
Run with --stacktrace option to get the stack trace. Run with --info or --debug option to get more log output.

BUILD FAILED

Total time: 0.532 secs
```

この場合は、V1.1__User_Data.sqlを元に戻すか、再度、V1からマイグレーションすればいい。

# 最後に
Flywayをうまく使えばシステム開発で必須のDB環境の管理が楽になる。なまじSQLのスキルがあるとテーブルを直接いじったりして、SQLスクリプトとして残らないことがある。たとえば、`ALTER TABE`を直接実施して環境を調整してしまうとか。これを良しとしていると、DB環境の再現性があやしくなる。

>最近やってたあのプロジェクトでそんな問題に遭遇したなぁ。
>今やってるあのプロジェクトに導入しておきたいなぁ。



# 参考

* [Flyway本家](http://flywaydb.org/)
* [FlywayでDBスキーマのマイグレーションをしてみた](https://siguniang.wordpress.com/2013/11/10/db-schema-migration-made-easy-with-flyway/)
* [Flyway使い方メモ](http://qiita.com/opengl-8080/items/6368c19a06521b65a655)
* [Flyway は複数人での開発に向かないという誤解について](http://tototoshi.hatenablog.com/entry/2015/01/27/234314)