golang-migrate/migrateとは
Goで書かれたデータベースマイグレーションツール
Go製のマイグレーションツールの中では、pressly/gooseと並んで人気がある様子
CLI, Goプロジェクト(パッケージとしてimportして使う)の両方から利用可能
- レポジトリ
golang-migrate/migrate
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
sqlファイル(up,down)を作成する
<version>_<name>.up.sql
と<version>_<name>.down.sql
の2つのsqlファイルを1セットとしてあつかう
たとえば
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>
として使うことにした
- いちいち自分でファイル名を書くのは面倒なのでシェルスクリプトを書きました
引数にとった文字列でを先頭につけた空ファイルを作成する
パッケージをimportしたバッチの作成
- 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/
なにがベストプラクティスなのか、わからないので
今後、探りながら開発していきたいです