データベースのマイグレーションを行うツール migrate を試した.migrate は MySQL / MariaDB / PostgreSQL / Amazon Redshift / MongoDB / Cassandra など,多くのデータベースに対応している.GitHub リポジトリを見るともっと多くのデータベースに対応していることがわかる.他にも GitHub や Amazon S3 に置かれたマイグレーションファイルを直接読み込む機能や Go ライブラリとしてアプリケーションに組み込める機能などもある.今回は MySQL を使ってマイグレーションの基本的な機能を試す❗️
インストール
macOS だと Homebrew を使って簡単に migrate CLI をインストールできる.今回は v4.15.2 を使う.
$ brew install golang-migrate $ migrate --version v4.15.2 $ migrate --help Usage: migrate OPTIONS COMMAND [arg...] migrate [ -version | -help ] 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] [-tz] 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. Use -tz option to specify the timezone that will be used when generating non-sequential migrations (defaults: UTC). goto V Migrate to version V up [N] Apply all or N up migrations down [N] [-all] Apply all or N down migrations Use -all to apply all down migrations drop [-f] Drop everything inside database Use -f to bypass confirmation force V Set version V but don't run migration (ignores dirty state) version Print current migration version Source drivers: godoc-vfs, gcs, s3, github-ee, go-bindata, file, bitbucket, github, gitlab Database drivers: cockroach, firebirdsql, sqlserver, clickhouse, crdb-postgres, mysql, pgx, postgresql, redshift, spanner, cassandra, mongodb, mongodb+srv, stub, cockroachdb, firebird, neo4j, postgres
準備
検証環境として使う MySQL 5.7 コンテナを起動しておく.今回はサクッと試すために MYSQL_ALLOW_EMPTY_PASSWORD
オプションなどを付けてある.またデータベースも作っておく.今回は MySQL 公式データセット を参考にするので,データベース名は world
にしておく.
$ docker run --name mysql57 -e MYSQL_ALLOW_EMPTY_PASSWORD=yes -p 3306:3306 -d mysql:5.7 $ docker exec -it mysql57 mysql -e 'CREATE DATABASE world'
最後にマイグレーションファイルを保存する db/migrate
ディレクトリも作っておく.
$ mkdir -p db/migrate
🧩 マイグレーション : 1 回目
migrate では migrate create
コマンドを使ってマイグレーションファイルを作る.--ext
オプションで拡張子は .sql
にする.--dir
オプションでディレクトリも指定する.そして --seq
オプションを付けると以下のようにファイル名に 000001
などの連番が自動採番される.今回は country
テーブルを追加するためマイグレーション名も country
にした.
up.sql
には CREATE TABLE
や ALTER TABLE ADD
など「前に進める SQL」を書く.down.sql
には DROP TABLE
や ALTER TABLE DROP COLUMN
など「戻す SQL」を書く.今回は MySQL サンプルを参考に country
テーブルの操作を書いた.
$ migrate create --ext sql --dir db/migrate --seq country /Users/kakakakakku/playground-migrate/db/migrate/000001_country.up.sql /Users/kakakakakku/playground-migrate/db/migrate/000001_country.down.sql
もし --seq
オプションを付けずにデフォルトのまま実行すると,以下のようにファイル名にタイムスタンプが付く.基本的には --seq
オプションを付けるのが良さそう.もし並行開発をしてて連番が重複してしまう場合や例外的にマイグレーションファイルの順番を変えたり途中に追加してデータベースを再構築する場合に影響範囲を抑えるためにタイムスタンプを使うというのはありそう(通常は前に進めるため過去日付は無視される).
$ migrate create --ext sql --dir db/migrate country /Users/kakakakakku/playground-migrate/db/migrate/20220925150000_country.up.sql /Users/kakakakakku/playground-migrate/db/migrate/20220925150000_country.down.sql
📝 000001_country.up.sql
CREATE TABLE `country` ( `Code` char(3) NOT NULL DEFAULT '', `Name` char(52) NOT NULL DEFAULT '', `Continent` enum('Asia','Europe','North America','Africa','Oceania','Antarctica','South America') NOT NULL DEFAULT 'Asia', `Region` char(26) NOT NULL DEFAULT '', `SurfaceArea` decimal(10,2) NOT NULL DEFAULT '0.00', `IndepYear` smallint DEFAULT NULL, `Population` int NOT NULL DEFAULT '0', `LifeExpectancy` decimal(3,1) DEFAULT NULL, `GNP` decimal(10,2) DEFAULT NULL, `GNPOld` decimal(10,2) DEFAULT NULL, `LocalName` char(45) NOT NULL DEFAULT '', `GovernmentForm` char(45) NOT NULL DEFAULT '', `HeadOfState` char(60) DEFAULT NULL, `Capital` int DEFAULT NULL, `Code2` char(2) NOT NULL DEFAULT '', PRIMARY KEY (`Code`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
📝 000001_country.down.sql
DROP TABLE `country`;
マイグレーションを実行する
さっそく migrate up
コマンドを使ってマイグレーションを実行する.--path
オプションにはディレクトリを指定する.--database
オプションには接続情報を指定する.すると,簡単に country
テーブルを追加できた!ちなみにマイグレーションの実行状況は schema_migrations
テーブルで管理されている.
$ migrate --path db/migrate --database 'mysql://root:@tcp(localhost:3306)/world' up 1/u country (18.581046ms) $ docker exec -it mysql57 mysql world -e 'SHOW TABLES;' +-------------------+ | Tables_in_world | +-------------------+ | country | | schema_migrations | +-------------------+ $ docker exec -it mysql57 mysql world -e 'SELECT * FROM schema_migrations;' +---------+-------+ | version | dirty | +---------+-------+ | 1 | 0 | +---------+-------+
マイグレーションを実行する(戻す)
今度は migrate down
コマンドを使ってマイグレーションを戻す.今回は引数に 1
を設定して1個戻した(ようするに country
テーブルを消すということ).
$ migrate --path db/migrate --database 'mysql://root:@tcp(localhost:3306)/world' down 1 1/d country (28.749487ms)
確認すると,ちゃんと country
テーブルが消えている!
$ docker exec -it mysql57 mysql world -e 'SHOW TABLES;' +-------------------+ | Tables_in_world | +-------------------+ | schema_migrations | +-------------------+
🧩 マイグレーション : 2 回目
次は city
テーブルを追加するためにマイグレーションファイルを作る.
$ migrate create --ext sql --dir db/migrate --seq city /Users/kakakakakku/playground-migrate/db/migrate/000002_city.up.sql /Users/kakakakakku/playground-migrate/db/migrate/000002_city.down.sql
📝 000002_city.up.sql
CREATE TABLE `city` ( `ID` int NOT NULL AUTO_INCREMENT, `Name` char(35) NOT NULL DEFAULT '', `CountryCode` char(3) NOT NULL DEFAULT '', `District` char(20) NOT NULL DEFAULT '', `Population` int NOT NULL DEFAULT '0', PRIMARY KEY (`ID`), KEY `CountryCode` (`CountryCode`), CONSTRAINT `city_ibfk_1` FOREIGN KEY (`CountryCode`) REFERENCES `country` (`Code`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
📝 000002_city.down.sql
DROP TABLE `city`;
マイグレーションを実行する
さっそく migrate up
コマンドを使ってマイグレーションを実行する.現在は migrate down
コマンドを使ってマイグレーションを戻した状態になっているため,今回は最新まで2個実行されている.
$ migrate --path db/migrate --database 'mysql://root:@tcp(localhost:3306)/world' up 1/u country (39.376667ms) 2/u city (62.559554ms) $ docker exec -it mysql57 mysql world -e 'SHOW TABLES;' +-------------------+ | Tables_in_world | +-------------------+ | city | | country | | schema_migrations | +-------------------+
🧩 マイグレーション : 3 回目
今度はテーブル追加ではなく country
テーブルに Favorite
カラムを追加するためにマイグレーションファイルを作る.
$ migrate create --ext sql --dir db/migrate --seq country_favorite /Users/kakakakakku/playground-migrate/db/migrate/000003_favorite.up.sql /Users/kakakakakku/playground-migrate/db/migrate/000003_favorite.down.sql
📝 000003_favorite.up.sql
ALTER TABLE `country` ADD `Favorite` BOOLEAN DEFAULT 0 NOT NULL AFTER `Code2`;
📝 000003_favorite.down.sql
ALTER TABLE `country` DROP COLUMN `Favorite`;
マイグレーションを実行する
期待通りに実行できた❗️
$ migrate --path db/migrate --database 'mysql://root:@tcp(localhost:3306)/world' up 3/u country_favorite (40.044104ms) $ docker exec -it mysql57 mysql world -e 'DESC country;' +----------------+---------------------------------------------------------------------------------------+------+-----+---------+-------+ | Field | Type | Null | Key | Default | Extra | +----------------+---------------------------------------------------------------------------------------+------+-----+---------+-------+ | Code | char(3) | NO | PRI | | | | Name | char(52) | NO | | | | | Continent | enum('Asia','Europe','North America','Africa','Oceania','Antarctica','South America') | NO | | Asia | | | Region | char(26) | NO | | | | | SurfaceArea | decimal(10,2) | NO | | 0.00 | | | IndepYear | smallint(6) | YES | | NULL | | | Population | int(11) | NO | | 0 | | | LifeExpectancy | decimal(3,1) | YES | | NULL | | | GNP | decimal(10,2) | YES | | NULL | | | GNPOld | decimal(10,2) | YES | | NULL | | | LocalName | char(45) | NO | | | | | GovernmentForm | char(45) | NO | | | | | HeadOfState | char(60) | YES | | NULL | | | Capital | int(11) | YES | | NULL | | | Code2 | char(2) | NO | | | | | Favorite | tinyint(1) | NO | | 0 | | +----------------+---------------------------------------------------------------------------------------+------+-----+---------+-------+
🧩 コマンド確認 : down
migrate down
コマンドを引数なく実行すると,全てのマイグレーションファイルを戻す挙動になる(ちゃんと y/N
の確認はある).以下のように実行すると 3 → 2 → 1 と戻って,テーブルは全て消えた.
$ migrate --path db/migrate --database 'mysql://root:@tcp(localhost:3306)/world' down Are you sure you want to apply all down migrations? [y/N] y Applying all down migrations 3/d country_favorite (29.164971ms) 2/d city (42.015304ms) 1/d country (54.650307ms)
🧩 コマンド確認 : goto
migrate up
コマンドを実行すると,基本的には最新のマイグレーションファイルまで実行する.もし「特定のマイグレーションファイルまで」実行する場合は migrate goto
コマンドを使う.以下は migrate goto 2
を実行したところ.
$ migrate --path db/migrate --database 'mysql://root:@tcp(localhost:3306)/world' goto 2 1/u country (17.119432ms) 2/u city (38.464471ms)
🧩 コマンド確認 : force
今度は「意図的に」誤ったマイグレーションファイルを作る.実際に開発中にはよくあることだと思う.今回は countrylanguage
テーブルを作るために新しくマイグレーションファイルを作る.
$ migrate create --ext sql --dir db/migrate --seq countrylanguage /Users/kakakakakku/playground-migrate/db/migrate/000004_countrylanguage.up.sql /Users/kakakakakku/playground-migrate/db/migrate/000004_countrylanguage.down.sql
📝 000004_countrylanguage.up.sql
以下の SQL では意図的に外部キーのカラム名を Code
ではなく CodeX
にして間違えている.
CREATE TABLE `countrylanguage` ( `CountryCode` char(3) NOT NULL DEFAULT '', `Language` char(30) NOT NULL DEFAULT '', `IsOfficial` enum('T','F') NOT NULL DEFAULT 'F', `Percentage` decimal(4,1) NOT NULL DEFAULT '0.0', PRIMARY KEY (`CountryCode`,`Language`), KEY `CountryCode` (`CountryCode`), CONSTRAINT `countryLanguage_ibfk_1` FOREIGN KEY (`CountryCode`) REFERENCES `country` (`CodeX`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
マイグレーションを実行する
migrate up
コマンドを実行すると,以下のように期待通りにエラーが出る.もう1度 migrate up
コマンドを実行すると,今度は Dirty database version 4. というエラーが出る.migrate では一度エラーが出るとそれ以上は進めないように実装されている.
$ migrate --path db/migrate --database 'mysql://root:@tcp(localhost:3306)/world' up 3/u country_favorite (25.492145ms) error: migration failed in line 0: CREATE TABLE `countrylanguage` ( `CountryCode` char(3) NOT NULL DEFAULT '', `Language` char(30) NOT NULL DEFAULT '', `IsOfficial` enum('T','F') NOT NULL DEFAULT 'F', `Percentage` decimal(4,1) NOT NULL DEFAULT '0.0', PRIMARY KEY (`CountryCode`,`Language`), KEY `CountryCode` (`CountryCode`), CONSTRAINT `countryLanguage_ibfk_1` FOREIGN KEY (`CountryCode`) REFERENCES `country` (`CodeX`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; (details: Error 1215: Cannot add foreign key constraint) $ migrate --path db/migrate --database 'mysql://root:@tcp(localhost:3306)/world' up error: Dirty database version 4. Fix and force version.
マイグレーションの実行状況を schema_migrations
テーブルで確認すると,以下のように dirty = 1
になってしまっている.
$ docker exec -it mysql57 mysql world -e 'SELECT * FROM schema_migrations;' +---------+-------+ | version | dirty | +---------+-------+ | 4 | 1 | +---------+-------+
そこで migrate force
コマンドを実行すると dirty = 0
に戻せるので,以下のように正常に実行できている状態まで migrate force 3
コマンドでリセットしている.あとはマイグレーションファイルを修正して,もう1度 migrate up
コマンドを実行すればリカバリ完了❗️
$ migrate --path db/migrate --database 'mysql://root:@tcp(localhost:3306)/world' force 3 $ docker exec -it mysql57 mysql world -e 'SELECT * FROM schema_migrations;' +---------+-------+ | version | dirty | +---------+-------+ | 3 | 0 | +---------+-------+
🧩 コマンド確認 : drop
日常的に実行するものではないけど,もしデータベースを完全に初期化する場合は migrate drop
コマンドを使う.もしかしたら migrate down
コマンドと似ているようにも思うけど,実装を確認すると MySQL の場合は SHOW TABLES LIKE '%'
を実行して取得したテーブルに対して DROP TABLE IF EXISTS
を実行している.よって,migrate drop
コマンドはマイグレーションファイルの「戻す SQL」を実行するのではなく強制的に初期化していると言える.
$ migrate --path db/migrate --database 'mysql://root:@tcp(localhost:3306)/world' drop Are you sure you want to drop the entire database schema? [y/N] y Dropping the entire database schema
🧩 コマンド確認 : up --source
migrate はマイグレーションファイルをファイルシステム以外から直接読み込む機能がある.例えば GitHub や Amazon S3 も対応してて便利!以下は migrate の GitHub リポジトリに含まれているマイグレーションファイルを直接 migrate up
コマンドを使って実行している.ファイルシステムに何もマイグレーションファイルを置かずに簡単に test
テーブルを追加できた!おー💡
$ migrate --source github://golang-migrate/migrate/database/mysql/examples/migrations --database 'mysql://root:@tcp(localhost:3306)/world' up 1/u init (29.417794ms) $ docker exec -it mysql57 mysql world -e 'SHOW FULL COLUMNS FROM test;' +-----------+-------------+-------------------+------+-----+---------+-------+---------------------------------+---------+ | Field | Type | Collation | Null | Key | Default | Extra | Privileges | Comment | +-----------+-------------+-------------------+------+-----+---------+-------+---------------------------------+---------+ | firstname | varchar(16) | latin1_swedish_ci | YES | | NULL | | select,insert,update,references | | +-----------+-------------+-------------------+------+-----+---------+-------+---------------------------------+---------+
まとめ
migrate 便利!