MySQLのmigrate

### はじめに

業務では専らMySQLを使用している。
スキーマ管理として、[migrate](https://github.com/golang-migrate/migrate)を用いており、この使用方法をメモ。

### 環境

* migrate v4.14.1
* MySQL 5.7

## インストール

下記本家サイトを参考にインストールしておく。

https://github.com/golang-migrate/migrate/tree/master/cmd/migrate

## OptionとCommand

`migrate -help`で確認できるが、OptionとCommandは以下のような感じ。

```
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:
  create [-ext E] [-dir D] [-seq] [-digits N] [-format] NAME
	   Create a set of timestamped up/down migrations titled NAME, in directory D with extension E.
	   Use -seq option to generate sequential up/down migrations with N digits.
	   Use -format option to specify a Go time format string. Note: migrations with the same time cause "duplicate migration version" error. 

  goto V       Migrate to version V
  up [N]       Apply all or N up migrations
  down [N]     Apply all or N down migrations
  drop [-f] [-all]    Drop everything inside database
	Use -f to bypass confirmation
	Use -all to apply all down migrations
  force V      Set version V but don't run migration (ignores dirty state)
  version      Print current migration version
```

## 使い方

以下のコマンドを実行すると、`0001_secual.up.sql`と`0001_secual.down.sql`のファイルが作成される。

```
migrate create -ext sql -seq -digits 4 secual
```

以下のコマンドだと、`202108171914_secual.up.sql`と`202108171914_secual.down.sql`が作成される。

```
migrate create -ext sql -format 200601021504 secual
```

* `*.up.sql`と対になる`*.down.sql`が必要
* ファイル名先頭は数値（0パディング）、もしくは`yyyyMMddHHmm`のような日付形式（この値でバージョン管理される）

#### 中身（例１）

`*.up.sql`には、DBスキーマに適用したい内容を書く。例えば、テーブル作成。

```sql:0001_secual.up.sql
CREATE TABLE article (
    id INT AUTO_INCREMENT NOT NULL PRIMARY KEY,
    title VARCHAR(50),
    content TEXT,
    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
```

`*.down.sql`には、`*.up.sql`を打ち消す内容を書く。例えば、`*.up.sql`がテーブル作成であればテーブル削除。

```sql:0001_secual.down.sql
DROP TABLE article;
```

#### 中身（例２）

例えば、カラム変更とカラム追加の場合。

```sql:0002_secual.up.sql
ALTER TABLE article MODIFY COLUMN title VARCHAR(100);
ALTER TABLE article ADD COLUMN author VARCHAR(50) AFTER content;
```

`*.down.sql`には、カラム削除とカラム変更。（`*.up.sql`の記載と**逆の順番**で、`*.down.sql`に記載するようにしている。整合性がとりやすいので。）

```sql:0002_secual.down.sql
ALTER TABLE article DROP COLUMN author;
ALTER TABLE article MODIFY COLUMN title VARCHAR(50);
```

#### 実行

##### バージョン確認

`*.up.sql`や`*.down.sql`があるディレクトリだと、以下のコマンドで現在のmigrateのバージョンが確認できる。

```
migrate -source file://./ -database 'mysql://user:passwd@tcp(localhost:3306)/media' version
```

* `-source`で、`*.up.sql`や`*.down.sql`のディレクトリを指定
* `-database`で、接続するデータベースを指定

`0002_secual.up.sql`まで適用されている場合は、`2`と表示される。


##### アップ適用

`*.up.sql`の内容を適用するには、以下のコマンドを実行する。

```
migrate -source file://./ -database 'mysql://user:passwd@tcp(localhost:3306)/media' up
```

:::note warn
適用されていない *.up.sql の内容がすべて適用される。（最大バージョンまで適用される。）
例えば、現在のバージョンが 0001 であり、0004_secual.up.sqlまで作成されている場合、0002→0003→0004の順ですべてが適用される。
:::


現在のバージョンが`1`であり、`0004_secual.up.sql`まで作成されているが、`0003_secual.up.sql`までを適用したい場合（２つバージョンを上げたい場合）は、以下のコマンドを実行する。


```
migrate -source file://./ -database 'mysql://user:passwd@tcp(localhost:3306)/media' up 2
```

##### ダウン適用

`*.down.sql`の内容を適用するには、以下のコマンドを実行する。

```
migrate -source file://./ -database 'mysql://user:passwd@tcp(localhost:3306)/media' down
```

:::note warn
最小バージョン（0001_secual.down.sql）までの内容がすべて適用される。
:::

現在のバージョンが`4`であり、`000２_secual.down.sql`までを適用したい場合（２つバージョンを下げたい場合）は、以下のコマンドを実行する。

```
migrate -source file://./ -database 'mysql://user:passwd@tcp(localhost:3306)/media' down 2
```

##### 強制的にバージョン変更

強制的にバージョンを`2`に変更する場合には以下のコマンドを実行する。主にアップ(up)やダウン(down)でエラーになった場合に用いる。

```
migrate -source file://./ -database 'mysql://user:passwd@tcp(localhost:3306)/media' force 2
```

migrateのバージョンのみを変えるだけであって、アップ(up)やダウン(down)のSQLは実行されないので、DBスキーマは不整合性な状態になっているはず。この場合は、SQLを別途実行して調整する必要がある。
例えば、以下のようなSQL（ADDのtypo）でアップ（up）した場合、１行目のMODIFYは実行され２行目でエラーになる。

```sql:0002_secual2.up.sql
ALTER TABLE article MODIFY COLUMN title VARCHAR(100);
ALTER TABLE article AD COLUMN author VARCHAR(50) AFTER content;
```

migrateのバージョンは、`1(dirty)` となる。以下のような対処が必要になる。

* `ALTER TABLE article ADD COLUMN author  VARCHAR(50) AFTER content;`を実行し正しいバージョン`2`の状態にして、migrateコマンドで`force 2`を実行する
* `ALTER TABLE article MODIFY COLUMN title VARCHAR(50);`を実行しバージョン`1`の状態に戻して、migrateコマンドで`force 1`を実行する
 
## 運用するにあたり

* バージョンとして数値と日付を選択できるが、数値のほうが管理しやすい（日付バージョンで運用したことないけど。。）
* 面倒だけど、順序はそれぞれで逆になるように、upとdownは対になるように書く
* １ファイルに多くを記述するのではなく、テーブル単位などで細かくファイルを分けるようにする（エラー時に対応が面倒になるので。。）
* ファイルは記述した人がローカル環境などで動作確認しておくべし
* ファイル名でバージョン管理することになるためGitとの相性が悪い。。（複数人がNASで共有編集する運用をしてます。。他に何かいいツールありますか？）