【golang】migrateでDBマイグレーションをする

go言語で実装したWebアプリケーションサイトのDBマイグレーションツールとしてgolang-migrate/migrateを使っていて、そこそこ便利につかえているので紹介してみたいと思います。

migrateを使うメリット

migrateは、Go言語で作成されたDBマイグレーションツールの1つです。

migrateを使うことで得られるメリットは

の3つかと思います。

migrateを使うことにした背景

golang のORMフレームワークgorm を使っているのですが、CLIが提供されていないためDBマイグレーションのタイミングをコントロールしづらいので、 migrate でカバーすることにしました。

migrate を採用する際の注意点としては、gormは独自のDSLスキーマ定義を記載することでDBシステム間(MySQLPostgreSQL等)の差異を吸収してくれますが、 migrateの場合は定義ファイルに書かれたDDLを実行するだけなので、DBシステムの差異を意識して定義ファイルを記載する必要があります。

環境/前提

  • Ubuntu 18.04 LTS デスクトップ
  • PostgreSQLインストール済
  • PostgreSQLに"sample"という名前のデータベース作成済

インストール

インストール方法が、README.mdにもGETTING_STARTED.md にも書いていないので、分かりづらいですが以下に記載されています(2021/1/13時点)。今後READMEに移動するかもしれないので、都度最新の情報を確認してください。

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

ビルド済のバイナリをインストールする方法やaptでもインストールできますが、今回はgo getでインストールしました。

$ go get -tags 'postgres' -u github.com/golang-migrate/migrate/cmd/migrate

使ってみる

最初は以下のPostgreSQLチュートリアルをやってみるのがてっとり早いかと思います。 migrate/TUTORIAL.md at master · golang-migrate/migrate · GitHub

マイグレーションの実行に失敗したらどうなるか等把握するために色々試してみたいので、チュートリアルにの内容にちょっと手を加えて進めてみます。

マイグレーションの定義ファイルを作る

1つ目のマイグレーションとして、ユーザーテーブルを作って、インデックスを貼ってみたいと思います。

を指定して migrate createを実行すると、000001_create_users.(up/down).sql という2つのSQLファイルができます。

$ migrate create -ext sql -dir db/migrations -seq create_users
~/golang-migrate-sample/db/migrations/000001_create_users.up.sql
~/golang-migrate-sample/db/migrations/000001_create_users.down.sql

作成されたup/down.sqlファイルにそれぞれ以下のようにCREATE TABLEDROP TABLEDDLを書いてみます。PostgreSQLDDLでもトランザクションが使えるので使ってみます。

BEGIN;
CREATE TABLE IF NOT EXISTS users(
   id serial PRIMARY KEY,
   username VARCHAR (50) UNIQUE NOT NULL,
   age INT NOTNULL
);
CREATE INDEX on users (age);
COMMIT;
DROP TABLE IF EXISTS users;

同様に、コメントテーブルのマイグレーションも書いてみます。

$ migrate create -ext sql -dir db/migrations -seq create_comments
~/golang-migrate-sample/db/migrations/000002_create_comments.up.sql
~/golang-migrate-sample/db/migrations/000002_create_comments.down.sql
CREATE TABLE IF NOT EXISTS comments(
   id INT PRIMARY KEY,
   body VARCHAR (50) NOT NULL,
   user_id INT NOT NULL
);
DROP TABLE IF EXISTS comments;

ファイル/ディレクトリ構成確認

ファイル/ディレクトリ構成としては以下のようになっている状態かと思います。

└── db
    └── migrations
        ├── 000001_create_users.down.sql
        ├── 000001_create_users.up.sql
        ├── 000002_create_comments.down.sql
        └── 000002_create_comments.up.sql

マイグレーションの実行

現状は以下の図のように、1と2の定義ファイルがあるだけでDBにはテーブルが1つもない状態です。 f:id:moritamorie:20210113022223p:plain

さっそくマイグレーションを実行してみたいと思います。

migrate up 1を実行し、ユーザーテーブルをCREATEする

PostgreSQLの接続URLを環境変数に入れて、バージョンを1つ上げてみます。

$ export POSTGRESQL_URL='postgres://postgres:password@localhost:5432/sample?sslmode=disable'
$ migrate -path db/migrations -database ${POSTGRESQL_URL} up 1

以下の図のように、000001_create_users.up.sqlだけが実行され、schema_migrationsテーブル(version: 1のレコードが1件)とusersテーブル(内容は空)ができました。

f:id:moritamorie:20210113022153p:plain

マイグレーションを実行するとschema_migrationsテーブルに

  • 現在のバージョン(version)
  • エラーが発生するとdirty: true、しなければ dirty: false

が保存されます。

migrate down 1を実行し、ユーザーテーブルをDROPする

同様にバージョンを1つ下げてみます。

$ migrate -path db/migrations -database ${POSTGRESQL_URL} down 1

すると以下の図のようにusersテーブル、とschema_migrationsテーブルのレコードがなくなり、DROPできたことが確認できます。

f:id:moritamorie:20210113022101p:plain

わざと失敗してみる

先程の000001_create_users.up.sqlを以下のように修正し、CREATE TABLEはそのままでCREATE INDEXは失敗するようにしてみます。

BEGIN;
CREATE TABLE IF NOT EXISTS users(
   id serial PRIMARY KEY,
   username VARCHAR (50) UNIQUE NOT NULL,
   age INT NOTNULL
);
CREATE INDEX on users, (age);
COMMIT;
$ migrate -path db/migrations -database ${POSTGRESQL_URL} up 1
error: migration failed: ","またはその近辺で構文エラー (column 22) in line 7: BEGIN;
CREATE TABLE IF NOT EXISTS users(
   id INT PRIMARY KEY,
   username VARCHAR (50) UNIQUE NOT NULL,
   age INT NOT NULL
);
CREATE INDEX on users, (age);
COMMIT;
 (details: pq: ","またはその近辺で構文エラー)

すると、以下のようにトランザクションが効いてユーザーテーブルが作られずに、 schema_migrationsdirtytrue になることが確認できます。

f:id:moritamorie:20210113020549p:plain

dirty: trueの状態を修正する方法をドキュメントから探していると forceを指定することで、dirtyfalse の状態(1つ前のバージョン)に強制的に変更できるという記載があったのでそれをやろうとしましたが、こちらのissueにバージョン1で失敗した場合はバージョン0にはできないと書いてあったので、仕方なく schema_migrationsファイルは手動で削除しました。

バージョン2以降で失敗した場合には、以下のように force バージョンを指定することで、 schema_migrationsvarsion: 1, dirty: falseの状態にできます。

$ migrate -path db/migrations -database ${POSTGRESQL_URL} force 1
migrate upを実行し、ユーザーテーブルとコメントテーブルを作成

最後にmigrate up 実行することで、未実行のマイグレーションが全て実行されることを確認しました。000001_create_users.up.sqlは修正は元に戻して実行しています。

$ migrate -path db/migrations -database ${POSTGRESQL_URL} up
1/u create_users (25.104164ms)
2/u create_comments (53.025987ms)

f:id:moritamorie:20210113021917p:plain

使ってみた所感

ちょっとしたハマりどころもあるので、そんなによく作り込まれているという感じではなく万人受けはしなさそうだけど、インタフェースがシンプル(up、down、forceくらい)なので使い勝手がよく

  • 小規模のシステム
  • 個人開発

といった使用用途には良い気がしました。少なくとも個人開発で使っている分には困るケースには当たってないです。

サンプル

作ったサンプルのURLを載せておきますので、よかったら参考にしてみてください! https://github.com/moritamori/golang-migrate-sample

参考資料