運用中のシステムにも導入可なGo製のマイグレーションツール「mattes/migrate」

データベースマイグレーションツールでフレームワーク非依存のもので有名所といえば[flyway]だが、Java製のツールであるためJVMを入れておく必要がある。JVMを入れるというのが意外とハードルで、稼働中の環境に入れる場合は影響範囲が怖かったり、CIのたびに容量が小さくないJVMを入れるのはちょっと・・・というケースもある。

そんなときにオススメしたいのがGo製のマイグレーションツール[mattes/migrate]だ。flywayと同じくCLIツールだが、JVMのようなランタイムは不要だ。ビルド済みのバイナリがGitHubで提供されていて、インストールはcurlやwgetですぐにできる。容量も7.1Mと小さい。MySQLやPostgreSQLなど主要なデータベースに対応している。Go製であるが、マイグレーションファイルは素のSQLで書くから、ユーザにはGo言語の知識は必要ない。

[flyway]: https://flywaydb.org/
[mattes/migrate]: https://github.com/mattes/migrate

## mattes/migrateにぴったりな利用シーン

* Webフレームワークを使ってない。もしくは、Webフレームワークにマイグレーション機能が無い。
* 運用フェーズに入ったシステムに後からマイグレーションの仕組みを導入したい。
* 使っている言語にベストなマイグレーションツールが存在しない。急成長中の比較的マイナーな言語とか。

ちなみに、JVMが入ることで環境が汚れるのだけが気になる、容量はさして重要じゃない、Dockerを使えるという環境であればflywayのDockerコンテナを使うという選択肢もある。[dhoer/flyway - Docker Hub](https://hub.docker.com/r/dhoer/flyway/)

## インストール

macOSでのインストール。([Homebrewは対応途中とのこと](https://github.com/mattes/migrate/issues/156)。今後に期待。)

```
cd /usr/local/bin
curl -L https://github.com/mattes/migrate/releases/download/v3.0.1/migrate.darwin-amd64.tar.gz | tar xvz
mv migrate.darwin-amd64 migrate
```

Linuxでのインストール。([apt-getでもインストールできる](https://github.com/mattes/migrate/tree/master/cli#linux-deb-package))

```
cd /usr/local/bin
curl -L https://github.com/mattes/migrate/releases/download/v3.0.1/migrate.linux-amd64.tar.gz | tar xvz
mv migrate.linux-amd64 migrate
```

## 今回紹介するバージョン

v3.0.1を紹介する。

```console
$ migrate --version
3.0.1
```

## ヘルプを見てみる

```console
$ migrate -help
Usage: migrate OPTIONS COMMAND [arg...]
       migrate [ -version | -help ]

Options:
  -source          Location of the migrations (driver://url)
  -path            Shorthand for -source=file://path
  -database        Run migrations against this database (driver://url)
  -prefetch N      Number of migrations to load in advance before executing (default 10)
  -lock-timeout N  Allow N seconds to acquire database lock (default 15)
  -verbose         Print verbose logging
  -version         Print version
  -help            Print usage

Commands:
  goto V       Migrate to version V
  up [N]       Apply all or N up migrations
  down [N]     Apply all or N down migrations
  drop         Drop everyting inside database
  force V      Set version V but don't run migration (ignores dirty state)
  version      Print current migration version
```

## マイグレーションをやってみる

マイグレーションファイル置き場を作る

```
mkdir ./sql
cd sql
```

スキーマのアップグレード用のマイグレーションファイルは`N_$ファイル名.up.sql`というファイル名にする。逆にダウングレード用は`N_$ファイル名.down.sql`といった命名規則。ここの`N`はマイグレーションバージョンを意味する。1から連番で始めてもいいし、20170518のように日付っぽくしてもいい。

```bash
cat > 1_Create_user_table.up.sql <<'SQL'
CREATE TABLE `users` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  PRIMARY KEY (`id`)
);
SQL
```

マイグレーションを実施するときは、`-database`で変更先DBをURL形式で指定する。URL形式のうち(`tcp(...)`はちょっと特殊な書き方で直感的でなかった)。`-path`はマイグレーションファイル`*.up.sql`が置いてあるディレクトリを指定する。

```console
$ migrate -database 'mysql://root:password@tcp(127.0.0.1:3306)/myapp' -path ./ up
1/u Create_user_table (21.823571ms)
```

状況を確認してみると、`users`テーブルとは別に、マイグレーションステータスを記録する`schema_migrations`テーブルが作られている。

```console
$ mysql -uroot  myapp -e 'SHOW TABLES'
+-------------------+
| Tables_in_myapp   |
+-------------------+
| schema_migrations |
| users             |
+-------------------+
```

このテーブルの中身はこんなかんじ。

```console
$ mysql -uroot myapp -e 'SELECT * FROM schema_migrations'
+---------+-------+
| version | dirty |
+---------+-------+
|       1 |     0 |
+---------+-------+
```

`migrate up`コマンドの操作は冪等性が保証するために、このテーブルが使われている。つまり、マイグレーションファイルに追加がない限り、何度`up`してもマイグレーションが重複して適用されることはない。

```console
$ migrate -database 'mysql://root@tcp(127.0.0.1:3306)/myapp' -path ./ up
no change
```

つぎに`ALTER TABLE`するマイグレーションファイルを作ってみる。

```bash
cat > 2_Add_username_to_users_table.up.sql <<'SQL'
ALTER TABLE `users` ADD `username` VARCHAR(255)  NOT NULL  DEFAULT '';
SQL
```

マイグレーションを走らせてみる。

```console
$ migrate -database 'mysql://root@tcp(127.0.0.1:3306)/myapp' -path ./ up
2/u Add_username_to_users_table (25.283128ms)
```

先程は、`schema_migrations`テーブルを覗いてマイグレーションバージョンを確認したが、`migrate version`コマンドでも確認できる。

```console
$ migrate -database 'mysql://root@tcp(127.0.0.1:3306)/myapp' -path ./ version
2
```

## 運用中のアプリに途中から導入する場合

運用中のアプリに途中からマイグレーションの仕組みを取り入れる場合を「ToDoアプリ」を例に考えてみよう。運用中のシステムには、既にユーザテーブルやToDoデータが登録されている。ここに、チームでToDoを管理できるようなチーミング機能が追加されたとしよう。本番環境にマイグレーションを導入するのもこのチーミング機能のリリースと同時期に行う計画だ。

たとえば、このような状況だ。

1. usersテーブル … 運用中・データあり。
* todosテーブル … 運用中・データあり。
* teamsテーブル __NEW!__ … 今回のアップデートで追加されるチーム機能。当然運用中システムにテーブルはない。ここからマイグレーション管理導入。

このようなケースでも、開発環境はゼロからセットアップすることがあるので、1〜2のテーブルについてもマイグレーションファイルにしておきたい。一方で、本番環境では3から適用したい。こういったニーズが想定される。

このような場合、どうやって管理したら良いか？

まずは、それぞれのテーブルをマイグレーションファイルに起こす。

```
1_Create_users_table.up.sql
2_Create_todos_table.up.sql
3_Create_teams_table.up.sql
```

これを、`migrate`でうまく扱うには`force`コマンドを使うとよい。`force`コマンドは特定のバージョンまでマイグレーションを適用したことにするコマンドだ。マイグレーションは走らない。

### 本番環境をマイグレーションに乗せる

本番環境は2まで適用したことにしたいので、マイグレーションの仕組み導入時、つまり初回に次のコマンドを走らせる。

```
migrate -database 'mysql://...' -path ./ force 2
```

`version`コマンドで、マイグレーション状況を確認してみよう。2になっているはずだ。

```console
$ migrate -database 'mysql://...' -path ./ version
2
```

そして、新機能のv3をリリースするときには`up`コマンドを流すようにする。その後のアップデート時も同様だ。

```console
$ migrate -database 'mysql://...' -path ./ up
3/u Create_teams_table (35.575331ms)
```

これで、本番環境もマイグレーションに乗ることができる。


## まとめ

[mattes/migrate]は、シングルバイナリで動作するデータベースマイグレーションツールで、運用中のシステムにマイグレーションの仕組みを導入するのに便利。