golang-migrate/migrateをパッケージとしてGoから使ってみる

# golang-migrate/migrateとは

Goで書かれたデータベースマイグレーションツール
Go製のマイグレーションツールの中では、[pressly/goose](https://github.com/pressly/goose)と並んで人気がある様子
CLI, Goプロジェクト（パッケージとしてimportして使う）の両方から利用可能

- レポジトリ  
[golang-migrate/migrate](https://github.com/golang-migrate/migrate)  
[mattes/migrate](https://github.com/mattes/migrate)をforkしたもので、もともとのmattes/migrateは非推奨になってメンテナンスすでに行われていない

CLIだとインストール環境によってインストールできる、できないとか問題起こってしまったり、コマンドを忘れる/ミスをする可能性も高いので
CLIからではなく、コード化するためにGoのパッケージとして利用してみた備忘録

# 環境
- AWS Cloud9 (シンガポールリージョンのEC2上で動作）
- Amazon Linux2
- zsh
- mysql

### Goのバージョン
Goのバージョンは1.11.5を使用する
1.11からパッケージマネージャーの`dep`ではなく`go mod`を使用してモジュールを管理できる
`dep`ではまだセマンティックバージョニングに対応していない
golang-migrate/migrateでは、今後優先的に`v4`のセマンティックバージョンからメンテナンスやアップデートが入るようなので、`go mod`を利用する

- go modについてはこの記事が詳しかった  
[Go 1.11 の modules・vgo を試す - 実際に使っていく上で考えないといけないこと #golang](https://www.wantedly.com/companies/wantedly/post_articles/132270#_=_)

# sqlファイル（up,down）を作成する
`<version>_<name>.up.sql`と`<version>_<name>.down.sql`の２つのsqlファイルを１セットとしてあつかう
たとえば

```
1_create_table.up.sql
1_create_table.down.sql
```

このとき、migrateは`<version>`しかみておらず、`<name>`の部分は、単純に可読性のみ気にしておけばよいので、sqlの中身がわかるような名前を付けると良さそう

### up
変更を加えるsqlファイルを記入する
例えば、`create table`文や`update`、`insert`文を書く

### down
upで変更を加えた状態から、変更前の状態に戻すクエリを書く
例えば、`create table`に対して`drop table`など

### version
1 → 2 → 3 ...のようにインクリメントしていってもよいし
201902101130 → 201902101230 のように日付時刻のタイムスタンプを使ってもよい
ようは、sqlファイルの順番がわかればいいので、昇順になるように設定する

今回はファイルを作成したときの日付時刻を`<version>`として使うことにした

- いちいち自分でファイル名を書くのは面倒なのでシェルスクリプトを書きました  
   [引数にとった文字列で<YmdHM>を先頭につけた空ファイルを作成する](https://qiita.com/tanden/items/23cd48e5385515c71ac1)


# パッケージをimportしたバッチの作成
- Goのパッケージとしてのドキュメントはこちら
    - https://godoc.org/github.com/golang-migrate/migrate

```go:migrate.go
package main

import (
	"flag"
	"fmt"
	"github.com/golang-migrate/migrate/v4"
	_ "github.com/golang-migrate/migrate/v4/database/mysql"
	_ "github.com/golang-migrate/migrate/v4/source/file"
	"os"
)

//sql and database info
const (
	Source   = "file://./sql/"
	Database = "mysql://user:password@tcp(0.0.0.0:3306)/database"
)

//declare command line options
var (
	Command = flag.String("exec", "", "set up or down as a argument")
	Force   = flag.Bool("f", false, "force exec fixed sql")
)

//available command list
var AvailableExecCommands = map[string]string{
	"up":      "Execute up sqls",
	"down":    "Execute down sqls",
	"version": "Just check current migrate version",
}

func main() {

	flag.Parse()
	if len(*Command) < 1 {
		fmt.Println("\nerror: no argument\n")
		showUsageMessge()
		os.Exit(1)
		return
	}

	m, err := migrate.New(Source, Database)
	if err != nil {
		fmt.Println("err", err)
	}
	version, dirty, err := m.Version()
	showVersionInfo(version, dirty, err)

	fmt.Println("command: exec", *Command)
	applyQuery(m, version, dirty)
}

//exec up or down sqls
//with force option if needed
func applyQuery(m *migrate.Migrate, version uint, dirty bool) {
	if dirty && *Force {
		fmt.Println("force=true: force execute current version sql")
		m.Force(int(version))
	}

	var err error
	switch *Command {
	case "up":
		err = m.Up()
	case "down":
		err = m.Down()
	case "version":
		//do nothing
		return
	default:
		fmt.Println("\nerror: invalid command '" + *Command + "'\n")
		showUsageMessge()
		os.Exit(1)
	}

	if err != nil {
		fmt.Println("err", err)
		os.Exit(1)
	} else {
		fmt.Println("success:", *Command+"\n")
		fmt.Println("updated version info")
		version, dirty, err := m.Version()
		showVersionInfo(version, dirty, err)
	}
}

func showUsageMessge() {
	fmt.Println("-------------------------------------")
	fmt.Println("Usage")
	fmt.Println("  go run migrate.go -exec <command>\n")
	fmt.Println("Available Exec Commands: ")
	for available_command, detail := range AvailableExecCommands {
		fmt.Println("  " + available_command + " : " + detail)
	}
	fmt.Println("-------------------------------------")
}

func showVersionInfo(version uint, dirty bool, err error) {
	fmt.Println("-------------------")
	fmt.Println("version  : ", version)
	fmt.Println("dirty    : ", dirty)
	fmt.Println("error    : ", err)
	fmt.Println("-------------------")
}
```

とりあえずupとdownと、ひつようであればforceを使えればよいので、上記のようなコードにしました
### up
```
% go run migrate.go -exec up
```

### down
```
% go run migrate.go -exec down
```

### -f (force option)
```
% go run migrate.go -exec up -f
```

# プロジェクト構成

```
% tree
.
├── create_sql.sh
├── go.mod
├── go.sum
├── migrate.go
└── sql/
```

なにがベストプラクティスなのか、わからないので
今後、探りながら開発していきたいです