DBマイグレーションツールを比較した phpmig, migrate, goose

## はじめに
フレームワーク無しの環境で開発をしている場合、手動でテーブル追加などの運用をしている現場も少なくないでしょう。手動運用をしていると

* いつテーブル・カラム追加されたか分からない
* 本番環境、複数の開発者の開発環境で、DB環境が一致していない

などの問題が起きます。これらは、マイグレーションツールを導入することで解決します。
今回は、フレームワークに依存しないDBマイグレーションツール３つを比較・検証します。

## ツール

以下の３つを比較します。

ツール名|必要環境
---|---
[phpmig](https://github.com/davedevelopment/phpmig)|PHP 5.3以上, Composer
[migrate](https://github.com/mattes/migrate)|Go
[goose](https://bitbucket.org/liamstask/goose/)|Go

## 前提
* Mac OSX Yosemite
* MySQL 5.6.36

## 検証
### インストール
インストールはどのツールも簡単です。

#### phpmig

```shell-session
# マイグレーションツールphpmig本体
$ composer require davedevelopment/phpmig

# DIコンテナツール
$ composer require pimple/pimple
```

#### migrate

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

#### goose

```shell-session
$ go get bitbucket.org/liamstask/goose/cmd/goose
```

### 初期設定
#### phpmig
`init`コマンドを実行することで、マイグレーションファイルを配備するディレクトリとDB接続情報を保持する設定ファイルが作成されます。

```shell-session
$ vendor/bin/phpmig init
+d ./migrations Place your migration files in here
+f ./phpmig.php Create services in here
```

ローカルのMySQLに接続するため、phpmig.phpファイルを修正します。`#{dbname}`,`#{username}`, `#{password}`を変更してください。

```php:phpmig.php
<?php

# phpmig.php

use Phpmig\Adapter;
use Pimple\Container;

$container = new Container();

$container['db'] = function () {
    $dbh = new PDO('mysql:dbname=#{dbname};host=127.0.0.1','#{username}','#{password}');
    $dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
    return $dbh;
};

$container['phpmig.adapter'] = function ($c) {
    return new Adapter\PDO\Sql($c['db'], 'migrations');
};

$container['phpmig.migrations_path'] = __DIR__ . DIRECTORY_SEPARATOR . 'migrations';

return $container;
```

`status`コマンドを実行して、以下が表示されれば、設定完了です。

```shell-session
$ vendor/bin/phpmig status

 Status   Migration ID    Migration Name
-----------------------------------------
```

#### migrate
DB接続設定を、[JSONやYAMLファイルから読み込む](https://github.com/mattes/migrate/tree/master/cli#reading-cli-arguments-from-somewhere-else)ことができるようですが、migrate組み込みの機能ではないので、使いづらい印象です。

#### goose
DB接続設定ファイルの雛形をコピーしてきますが、初期値がPostgreSQL用になっているため、MySQL用に修正する必要があります。しかし残念なことに、ドキュメントにはMySQLの設定方法は解説されていません。

```shell-session
$ mkdir db
$ cp /Users/hypermkt/src/bitbucket.org/liamstask/goose/db-sample/
dbconf.yml db/
```

ソースコードの `lib/goose/dbconf.go` によると、driverには、mysqlとmymysqlの２つが設定できることが分かります。特に理由は無いですが、mymysqlの場合ですと、以下の書式で設定できます。（好きな方を選べということだろうが、一つにしてほしい・・）

```yaml:db/dbconf.yml
development:
    driver: mymysql
    open: tcp:localhost:3306*#{dbname}/#{username}/#{password}
```

### マイグレーション
それでは、各ツールのマイグレーション機能を見ていきましょう。

#### phpmig

generateコマンドを利用して、マイグレーションファイルを作成します。

```shell-session
$ vendor/bin/phpmig generate AddPosts
+f ./migrations/20170610153605_AddPosts.php
```

up/downの関数が定義された雛形が作成されます。

```php:./migrations/20170610153605_AddPosts.php
<?php

use Phpmig\Migration\Migration;

class AddPosts extends Migration
{
    /**
     * Do the migration
     */
    public function up()
    {
    }

    /**
     * Undo the migration
     */
    public function down()
    {
    }
}

```

ドキュメントの[こちら](https://github.com/davedevelopment/phpmig#writing-migrations)を参考に、以下のようにSQLを記述します。

```php:./migrations/20170610153605_AddPosts.php
<?php

use Phpmig\Migration\Migration;

class AddPosts extends Migration
{
    /**
     * Do the migration
     */
    public function up()
    {
        $sql = "
            CREATE TABLE posts(
                `id` integer NOT NULL AUTO_INCREMENT,
                `content` TEXT NOT NULL,
                `created_at` datetime DEFAULT CURRENT_TIMESTAMP(),
                PRIMARY KEY (`id`)
            );
";
        $container = $this->getContainer(); 
        $container['db']->query($sql);
    }

    /**
     * Undo the migration
     */
    public function down()
    {
        $sql = "DROP TABLE posts;";
        $container = $this->getContainer(); 
        $container['db']->query($sql);
    }
}
```

`status`コマンドで、バージョン = Migration IDを確認します。

```shell-session
$ vendor/bin/phpmig status

 Status   Migration ID    Migration Name
-----------------------------------------
   down  20170610153605  AddPosts
```

`upコマンド`の引数に、先程調べたバージョンを指定します。`-vvv`は、より多くの情報を表示するデバッグモードにつき、無くても動作します。これでテーブル作成が実行できます。

```shell-session
$ vendor/bin/phpmig up -vvv 20170610153605
 == 20170610153605 AddPosts migrating
 == 20170610153605 AddPosts migrated 0.0315s
```

#### migrate

マイグレーションファイルは、以下の書式で作成します。
`#{タイムスタンプ又は連番}_#{名前}.#{up又はdown}.sql`

[こちら](https://github.com/mattes/migrate/blob/master/FAQ.md#why-two-separate-files-up-and-down-for-a-migration)のFAQによると、この仕様には、新しい記述や文法を学ぶ必要がなく、簡単だからとのことです。

```sql:1_Create_Posts.up.sql
CREATE TABLE posts(
  `id` integer NOT NULL AUTO_INCREMENT,
  `content` TEXT NOT NULL,
  `created_at` datetime DEFAULT CURRENT_TIMESTAMP(),
  PRIMARY KEY (`id`)
);
```

以下オプションを指定することで、テーブルの作成が実行されます。

* `-verbose` ログを出力
* `-database` データベース接続設定を指定
* `-path` マイグレーションファイルのパスを指定
* `up` upマイグレーションを実行

```shell-session
$ migrate -verbose -database  'mysql://#{username}:#{password}@tcp(127.0.0.1:3306)/#{dbname}' -path ./ up
2017/06/11 00:54:13 Start buffering 1/u Create_Posts
2017/06/11 00:54:13 Read and execute 1/u Create_Posts
2017/06/11 00:54:13 Finished 1/u Create_Posts (read 20.324089ms, ran 15.589152ms)
2017/06/11 00:54:13 Finished after 37.83673ms
2017/06/11 00:54:13 Closing source and database
```

migrateの特徴としては、`force`コマンドを利用することで、指定バージョンまでスキップさせることができます。その際のマイグレーションは実行されます。例えば、以下２つのマイグレーションファイルがあったとし、既存DBではpostsテーブルが作成済みとします。

* 1_Create_Posts.up.sql
    * postsテーブルを作成する
* 2_Create_Users.up.sql
    * usersテーブルを作成する

早速試します。`force`コマンドにオプション1を渡すことで、バージョンを1に変更にし、1は実行済みの体としてスキップできます。

```shell-session
$ migrate -verbose -database  'mysql://root:@tcp(127.0.0.1:3306)/migration_migrate' -path ./ force 1
2017/06/12 00:09:22 Finished after 18.110456ms
2017/06/12 00:09:22 Closing source and database
```

`version`コマンドで確認した所、`1`になりました。

```shell-session
 migrate -verbose -database  'mysql://root:@tcp(127.0.0.1:3306)/migration_migrate' -path ./ version
2017/06/12 00:09:34 1
2017/06/12 00:09:34 Closing source and database
```

その状態で、`up`コマンドを実行すると、2番だけが実行されます。

```shell-session
$ migrate -verbose -database  'mysql://root:@tcp(127.0.0.1:3306)/migration_migrate' -path ./ up
2017/06/12 00:09:38 Start buffering 2/u Create_Users
2017/06/12 00:09:38 Read and execute 2/u Create_Users
2017/06/12 00:09:38 Finished 2/u Create_Users (read 21.020083ms, ran 15.266951ms)
2017/06/12 00:09:38 Finished after 37.777123ms
2017/06/12 00:09:38 Closing source and database
```

#### goose

`craete`コマンドに`sql`オプションを渡すことで、SQLファイルでマイグレーションファイルが作成されます。

```shell-session
$ goose create CreatePosts sql
goose: created #{省略}/db/migrations/20170611012402_CreatePosts.sql
```

作成されたマイグレーションファイルの、Up/Downの箇所に各々のSQL文を記述します。

```shell-session

-- +goose Up
-- SQL in section 'Up' is executed when this migration is applied

CREATE TABLE posts(
  `id` integer NOT NULL AUTO_INCREMENT,
  `content` TEXT NOT NULL,
  `created_at` datetime DEFAULT CURRENT_TIMESTAMP(),
  PRIMARY KEY (`id`)
);

-- +goose Down
-- SQL section 'Down' is executed when this migration is rolled back

DROP TABLE posts;
```

`up`コマンドを指定することで、テーブル作成が実行されます。

```shell-session
$ goose up
goose: migrating db environment 'development', current version: 0, target: 20170611012402
OK    20170611012402_CreatePosts.sql
```

## まとめ
各ツールを3点(1:悪い/難しい 2:普通 3:良い/簡単）で評価してみます。

||phpmig|migrate|goose
|---|---|---|---
|ドキュメントの充実性|3|2|2
|インストール難易度|3|3|3
|学習コスト|1|3|3
|機能性|2|2|2
|将来性|3|3|1
|合計|12|13|11

各ツールごとの感想は以下のとおりです。

* phpmig
    * ドキュメントは一番充実していました。ただコーディング量やpimpleの依存も考慮すると、学習コストがほかと比べて高い印象です。
* migrate
    * 都度DB設定を記述する手間はありますが、本番環境で導入を考慮すると、一番実用性がありました。
* goose
    * PostgreSQLのドキュメントは充実していますが、それ以外がコードを見ないと分からないのが不親切でした。また開発も2015年1月を最後に止まっています。機能面では一番シンプルで使い勝手が良かったのですが、残念でした。

100%満足したわけではありませんが、総合的にはmigrateが、一番良かったので今後使っていきたいという結論になりました。