goose(pressly/goose)は、Go言語で書かれたデータベースマイグレーションツールです。SQLファイルとGoの関数の両方でスキーマ変更を書き、goose up・goose downなどのコマンドで適用と巻き戻しを行います。本記事の出力はすべて、2026年10月11日公開のv3.29.0をGo 1.27.2でビルドし、SQLiteとPostgreSQL 18.6で実行して取得したものです。基本コマンドに加えて、チーム開発で起きるバージョン番号の衝突と、複数のアプリが同時にマイグレーションを走らせたときの競合まで扱います。
まとめ
- 最初に覚えるのは4つ:
goose create(ファイル作成)→goose up(適用)→goose status(状態確認)→goose down(1つ戻す)。 - 接続情報は環境変数か.env:
GOOSE_DRIVER・GOOSE_DBSTRING・GOOSE_MIGRATION_DIRを置けば、毎回DSNを打たずに済みます。.envは既定で読み込まれます。 - v3.28.0からGo 1.26以上が必要:古いGoに固定されたCIでは
@v3.27.3(Go 1.25.7以上)に版を固定します。 - Goで書いたマイグレーションは自前のバイナリで実行:
go installしたgooseはGoの関数を実行できません。migrationsパッケージを取り込みgoose.RunContextを呼ぶ小さなコマンドを作ります。 - 複数人開発はタイムスタンプ+goose fix:開発中はタイムスタンプ、本番に出す前にCIで連番へ変換します。適用済みのタイムスタンプ番号をfixで連番に変えると、改名されたファイルがPending扱いになります。
- アプリ起動時に適用するならProvider APIとロック:CLIと従来のグローバル関数にはロックがありません。2台が同じ未適用マイグレーションを同時に処理すると、競合によって片方が失敗する可能性があります。
以下、インストールから本番運用の順に、実行結果とあわせて説明します。
Gooseとは:pressly/gooseの位置付けと同名ツールの区別
「goose」で検索すると、名前が同じ別のソフトウェアが3つ見つかります。本記事が扱うのは1行目のpressly/gooseです。
| 名前 | 用途 | 言語 | リポジトリ | 最新版(2026年10月) |
|---|---|---|---|---|
| goose(本記事) | DBマイグレーション | Go | pressly/goose | v3.29.0 |
| Goose | 負荷テスト | Rust | tag1consulting/goose | 0.18.1 |
| goose | AIエージェント | Rust | aaif-goose/goose(旧block/goose) | v1.56.0 |
Rust製の負荷テストツールを探している場合はGoose(Rust)の使い方|負荷テストの導入手順・コマンド引数・メトリクスの読み方を参照してください。
pressly/gooseはMITライセンスのOSSで、2012年のLiam Staskawicz氏の実装を元に、Vojtech Vitek氏とMichael Fridman氏が開発を続けています。v3.29.0のCLIが受け付けるドライバはpostgres・mysql・sqlite3・spanner・mssql・azuresql・redshift・tidb・clickhouse・ydb・starrocks・tursoの12種類です。azuresql(Microsoft Entra ID認証)はv3.28.0で追加されました。v3.29.0の変更は、YDBの接続文字列でuse_env_credentialsを指定すると環境変数の認証情報を使えるようになった点と依存ライブラリの更新だけです(v3.29.0のリリースノート)。
インストールと接続設定:go install・Homebrew・環境変数の優先順位
go install・brewでの導入とGo 1.26の要件
Goが入っていればgo installで入ります。macOSならHomebrewでも入り、2026年10月28日時点のformulaは3.29.0です。Go自体の導入はGo言語とは|1.27系の導入からHTTP APIを動かすまでの実装入門にまとめています。
go install github.com/pressly/goose/v3/cmd/goose@latest
# macOS
brew install goose
goose --version
# goose version: v3.29.0
v3.28.0以降のgo.modはgo 1.26.0を宣言しており、CHANGELOGのv3.28.0の項にも「Minimum Go version is now 1.26」とあります。v3.29.0も同じ要件です。v3.27.0の時点では1.25で、v3.27.3のgo.modはgo 1.25.7です。CIのGoを1.25系に固定している場合は[email protected]のように版を指定してください。使わないドライバを外して小さく作るなら、リポジトリでgo build -tags='no_mysql no_mssql no_clickhouse' -o goose ./cmd/gooseのようにビルドタグを付けます。
接続設定:引数・環境変数・.envの優先順位
基本形はgoose DRIVER DBSTRING [OPTIONS] COMMANDです。毎回DSNを打つのは手間なので、実務では環境変数か.envファイルに寄せます。
# .env(カレントディレクトリに置くと既定で読み込まれる)
GOOSE_DRIVER=postgres
GOOSE_DBSTRING=postgres://app:secret@localhost:5432/app?sslmode=disable
GOOSE_MIGRATION_DIR=./migrations
GOOSE_TABLE=goose_db_version
優先順位はフラグ、環境変数、既定値の順です。.envとシェルの環境変数が両方あるときは、シェル側が勝ちます。.envにGOOSE_DBSTRING=./app.dbを書いたままGOOSE_DBSTRING=./other.db goose statusを実行すると、other.dbが新しく作られました。.envを読ませたくない場合は-env=none、別名のファイルを読ませる場合は-env=.env.stagingのように指定します。本番のDSNは.envに書かず、CIやコンテナの環境変数から渡す運用にしておくと、このシェル側優先の挙動とも噛み合います。
実際にどの値が使われるかはgoose envで表示できます。SQLite用の.envを置いたディレクトリで実行した結果です。
$ goose env
GOOSE_DRIVER="sqlite3"
GOOSE_DBSTRING="./app.db"
GOOSE_MIGRATION_DIR="./migrations"
GOOSE_TABLE=""
NO_COLOR="false"
パスワード入りのDSNもそのまま表示されるので、CIのログにgoose envの出力を残さないでください。GOOSE_TABLEが空のときは既定のgoose_db_versionが使われます。
マイグレーションファイルの作成:SQLとGoの2形式
goose createのファイル名:UTCタイムスタンプと-sの連番
goose create NAME sqlはマイグレーションディレクトリにファイルを作ります。日本時間10月28日05:04に実行すると20261027200405_create_posts.sqlになりました。ファイル名のタイムスタンプはUTCです。-sを付けると00001_add_title.sqlのような連番になります。
$ goose create create_posts sql
2026/10/28 05:04:05 Created new file: migrations/20261027200405_create_posts.sql
$ goose -s create add_title sql
2026/10/28 05:04:05 Created new file: migrations/00001_add_title.sql
1つのディレクトリにタイムスタンプと連番を混ぜると、連番(1)がタイムスタンプより必ず前に並びます。どちらで番号を振るかは後述の「チーム開発のバージョン衝突」で決めます。
SQLite用SQLマイグレーションとアノテーション
以下はSQLite用の例です。実行前に接続設定をGOOSE_DRIVER=sqlite3、GOOSE_DBSTRING=./app.dbへ変更してください。直前のPostgreSQL用設定では、このトリガー構文を実行できません。
SQLファイルは-- +goose Upと-- +goose Downのコメントで区切ります。Upはちょうど1つ必要で、Downは省略できます(書く場合はUpより後)。v3.27.0以降、goose createが出すテンプレートからStatementBegin/StatementEndが外れました。セミコロンを複数含むトリガーや関数を書くときだけ、自分で囲みます。
-- +goose Up
CREATE TABLE users (
id INTEGER PRIMARY KEY,
email TEXT NOT NULL UNIQUE,
name TEXT NOT NULL DEFAULT ''
);
-- +goose StatementBegin
CREATE TRIGGER users_lower_email AFTER INSERT ON users
BEGIN
UPDATE users SET email = lower(NEW.email) WHERE id = NEW.id;
END;
-- +goose StatementEnd
-- +goose Down
DROP TRIGGER users_lower_email;
DROP TABLE users;
ほかによく使うアノテーションは2つです。
-- +goose NO TRANSACTION:ファイル全体をトランザクションの外で実行します。PostgreSQLのCREATE INDEX CONCURRENTLYはトランザクション内で実行できないため、これを付けたファイルに分けます。-- +goose ENVSUB ON:以降の${ADMIN_EMAIL}のような記述を環境変数で置換します。既定は無効で、ENVSUB OFFかファイル末尾まで有効です。
書いたファイルはgoose -v validateで、DBに触れずに検査できます。アノテーションの無いファイルはfailed to parse migration: unexpected state 0で終了コード1になるので、CIの最初に置く価値があります。
$ goose -v validate
Type Txn Up Down Name
──── ─── ── ──── ────
sql ✔ 2 2 20260901000000_create_users.sql
sql ✔ 1 1 20260910000000_add_name.sql
sql ✘ 1 1 20260915000000_email_index.sql
Up・Down列はファイル内の文の数です。上のトリガー付きのファイルは、CREATE TABLEとStatementBegin〜Endで囲んだトリガーで2文と数えられています。Txn列が✘の行はNO TRANSACTIONを付けたファイルです。
Goマイグレーション:SQLiteの既存行の加工
以下はSQLiteの少量データ向けの例です。対象行をすべてメモリに保持し、1つのトランザクション内で更新します。大量データでは分割処理と再実行方法を別途設計してください。SQL中のプレースホルダーは?を使います。PostgreSQLへ移植する場合は$1・$2へ変更してください。
SQLだけで書きにくい変換、たとえばメールアドレスの@より前を新しいnameカラムに埋める処理は、Goの関数で書けます。ファイル名は数字で始め、_test.goで終わらせないのが条件です。
package migrations
import (
"context"
"database/sql"
"errors"
"strings"
"github.com/pressly/goose/v3"
)
func init() {
goose.AddMigrationContext(upBackfillName, downBackfillName)
}
func upBackfillName(ctx context.Context, tx *sql.Tx) error {
rows, err := tx.QueryContext(ctx, `SELECT id, email FROM users WHERE name = ''`)
if err != nil {
return err
}
type row struct {
id int64
email string
}
var targets []row
for rows.Next() {
var r row
if err := rows.Scan(&r.id, &r.email); err != nil {
rows.Close()
return err
}
targets = append(targets, r)
}
if err := rows.Err(); err != nil {
rows.Close()
return err
}
if err := rows.Close(); err != nil {
return err
}
for _, r := range targets {
name, _, _ := strings.Cut(r.email, "@")
if _, err := tx.ExecContext(ctx, `UPDATE users SET name = ? WHERE id = ?`, name, r.id); err != nil {
return err
}
}
return nil
}
func downBackfillName(ctx context.Context, tx *sql.Tx) error {
// どの行を埋めたかを記録していないため、自動では安全に戻せない
return errors.New("backfill cannot be reversed safely; use a forward migration")
}
Downをエラーにしているのは、Upで埋めた行と、もともと値が入っていた行や後から利用者が書き換えた行を区別できないためです。UPDATE users SET name = ''のように全行を空に戻すDownは、データを消すだけで逆操作になりません。AddMigrationContextはトランザクション内、AddMigrationNoTxContextはトランザクション外で動きます。
行の加工をアプリの構造体で扱いたくなっても、マイグレーション内ではGORMなどのモデルを使わず、生のSQLで書いてください。モデルの変更によって、古いマイグレーションがコンパイルできなくなったり、生成されるSQLが変わったりする可能性があります。GORMの一括挿入の書き方はGORM CreateInBatchesの一括挿入とトランザクション範囲|v1.31.2の基本操作で扱っています。
Goマイグレーションを動かす自前のバイナリ:goose.RunContext
go installで入れたgooseコマンドは、Goマイグレーションを実行できません。Goの関数はコンパイルして初めて登録されるためです。この例ではモジュール名をexample.com/gappとし、github.com/pressly/goose/v3とmodernc.org/sqliteを依存関係に追加します。既存プロジェクトではimport内のモジュール名を自分のものに置き換えます。そのうえでmigrationsパッケージを取り込んだ自前のコマンドを作り、goose.RunContextにサブコマンドを渡します。公式リポジトリのexamples/go-migrationsも同じ構成です。
// cmd/migrate/main.go
package main
import (
"context"
"database/sql"
"flag"
"log"
"github.com/pressly/goose/v3"
_ "modernc.org/sqlite"
_ "example.com/gapp/migrations" // init()でGoマイグレーションを登録させる
)
func main() {
dir := flag.String("dir", "migrations", "マイグレーションのディレクトリ")
dsn := flag.String("dsn", "app.db", "SQLiteのファイル")
flag.Parse()
args := flag.Args()
if len(args) == 0 {
log.Fatal("usage: migrate [-dir DIR] [-dsn DSN] COMMAND [ARGS...]")
}
db, err := sql.Open("sqlite", *dsn)
if err != nil {
log.Fatal(err)
}
defer db.Close()
if err := goose.SetDialect("sqlite3"); err != nil {
log.Fatal(err)
}
if err := goose.RunContext(context.Background(), args[0], db, *dir, args[1:]...); err != nil {
log.Fatal(err)
}
}
go build -o migrate ./cmd/migrateで作ったバイナリは、up・down・statusなどRunContextが対応するサブコマンドを受け付けます。CLI専用のenv・validateには対応しません。SQLの00001_create_users.sql(前掲のトリガー付きSQLのUp末尾にINSERT INTO users (email) VALUES ('[email protected]');を追加)と00002_backfill_name.goを置いて実行した結果です。
$ ./migrate up
OK 00001_create_users.sql (52.61ms)
OK 00002_backfill_name.go (2.6ms)
goose: successfully migrated database to version: 2
$ ./migrate down
ERROR go migration: "00002_backfill_name.go": failed to run go migration: backfill cannot be reversed safely; use a forward migration
$ ./migrate version
goose: version 2
INSERTした[email protected]の行は[email protected] name=taroになりました(emailの小文字化は前掲のトリガーによるもの)。Downがエラーを返すと終了コード1で止まり、バージョンは2のまま残ります。
デプロイ先には.goファイルを置く必要がありません。バイナリと.sqlだけをコピーした別ディレクトリでstatusを実行しても、00002_backfill_name.goはPendingとして一覧に出て、upで適用されました。登録済みのGo関数はバイナリの中にあるためです。
goose up・down・statusの実行結果:コマンド別の動き
v3.29.0のgoose -hに並ぶコマンドは13個です。
| コマンド | 動作 |
|---|---|
up |
未適用をすべて適用 |
up-by-one |
次の1つだけ適用 |
up-to VERSION |
指定バージョンまで適用 |
down |
最新を1つ戻す |
down-to VERSION |
指定バージョンまで戻す |
redo |
最新を戻して再適用 |
reset |
すべて戻す |
status |
各ファイルの適用状況 |
version |
現在のバージョン |
create NAME [sql|go] |
ファイル作成 |
env |
解決後の設定を表示 |
fix |
タイムスタンプを連番へ変換 |
validate |
実行せずに検査 |
upの前後でstatusを見ると、Pendingが適用日時に変わります。
$ goose status
Applied At Migration
=======================================
Pending -- 20260901000000_create_users.sql
Pending -- 20260910000000_add_name.sql
$ goose up
OK 20260901000000_create_users.sql (2.17ms)
OK 20260910000000_add_name.sql (1.76ms)
goose: successfully migrated database to version: 20260910000000
$ goose status
Applied At Migration
=======================================
Tue Oct 27 20:04:04 2026 -- 20260901000000_create_users.sql
Tue Oct 27 20:04:04 2026 -- 20260910000000_add_name.sql
このSQLite実行例のApplied AtはUTCです(日本時間では10月28日05:04)。表示時刻はDBに保存されたタイムスタンプとドライバの扱いに依存し、gooseが一律にUTCへ変換するわけではありません。
ロールバック:down・down-to・redo・resetの戻り幅
戻す幅で4つを使い分けます。downは1つ、down-to 20260901000000はそのバージョンが最新になるまで、down-to 0とresetは全部です。redoは最新1つのDownとUpを続けて実行するので、Downの書き漏れを見つけるのに向きます。
$ goose down
OK 20260910000000_add_name.sql (2.01ms)
$ goose version
goose: version 20260901000000
2件とも適用した状態からdown-to 0を実行すると、新しい順に全件が戻ります。
$ goose down-to 0
OK 20260910000000_add_name.sql (1.75ms)
OK 20260901000000_create_users.sql (22.29ms)
goose: no migrations to run. current version: 0
Downは「逆向きのSQL」であって、バックアップではありません。ALTER TABLE users DROP COLUMN nameを含むDownを本番で実行すると、そのカラムの値は戻りません。本番でデータを消す変更を取り消すときは、Downを流すより、値を残したまま前向きの修正マイグレーションを追加する方が安全です。Downは開発環境とステージングでredoを回すための道具と割り切るのが実務的です。
goose_db_versionテーブルに残る記録
適用履歴はgoose_db_versionテーブルに入ります。初回実行時にversion 0の行が作られ、以降は適用ごとに1行ずつ増えます。上のup→down→up→down-to 0のあと、もう一度upした状態です。
$ sqlite3 app.db 'select * from goose_db_version'
1|0|1|2026-10-27 20:04:04
5|20260901000000|1|2026-10-27 20:04:05
6|20260910000000|1|2026-10-27 20:04:05
idが2〜4の行はありません。gooseはDownを実行すると、そのバージョンの行を削除します。「いつ戻したか」の履歴はこのテーブルに残らないので、監査が必要ならCIのログで残してください。
列はid・version_id・is_applied・tstampの4つです。v3.28.0以降は、新規に作るMySQL/TiDBのテーブルでtstampがTIMESTAMPからDATETIMEに変わりました。TIMESTAMPは2038年1月19日までしか持てないためです。既存テーブルは自動では変わらないので、リリースノートにあるALTER TABLE goose_db_version MODIFY tstamp datetime NULL DEFAULT CURRENT_TIMESTAMP;を自分で流します。テーブル名は-tableかGOOSE_TABLEで変えられます。
チーム開発のバージョン衝突:missing migrationsとgoose fixの使いどころ
複数人がタイムスタンプでファイルを作ると、日付の古いファイルが後からマージされることがあります。9月15日のファイルまで適用済みのDBに、別ブランチで作った9月5日のファイルが入ってきた状況を再現すると、upはエラーで止まりました。
$ goose up
goose run: error: found 1 missing migrations before current version 20260915000000:
version 20260905000000: migrations/20260905000000_create_posts.sql
-allow-missingを付ければ適用されますが、実行順がファイル名の順と食い違います。既存DBでは9月15日→9月5日、新規DBでは9月5日→9月15日の順になるため、両方の順序で同じ結果になることを確認する必要があります。本番では-allow-missingを常用しないでください。
pressly/gooseのREADMEが推奨するのは「Hybrid Versioning(ハイブリッドバージョニング)」です。開発中はgoose createの既定どおりタイムスタンプで作り、本番に出す直前にgoose fixで連番へ変換します。
$ goose fix
RENAMED 20260901000000_create_users.sql => 00001_create_users.sql
RENAMED 20260905000000_create_posts.sql => 00002_create_posts.sql
RENAMED 20260910000000_add_name.sql => 00003_add_name.sql
RENAMED 20260915000000_email_index.sql => 00004_email_index.sql
落とし穴は、fixを実行するタイミングです。タイムスタンプ版を適用済みのDBに向けてfix後のファイルでstatusを見ると、4件すべてがPendingになり、upはfound 4 missing migrations before current version 20260915000000で失敗しました。DBには20260915000000が記録されているのに、ファイル側は1〜4に改名されているためです。続けてgoose resetを実行すると、終了コード0で終わるのに何も戻らず、goose versionは20260915000000のままでした。
運用ルールは次の2点に絞れます。
- fixはmainブランチへのマージ時にCIで1回だけ実行し、改名後のファイルをコミットする。本番とステージングには連番しか適用しない。
- タイムスタンプ版を適用済みのローカルDBは、fix後のファイルで
resetしても元に戻りません。不要な開発用DBを再作成して連番版を適用するか、改名前のファイルでresetしてから連番版を適用します。本番相当のデータを入れたDBはテストコンテナで使い捨てにしておくと楽です(Testcontainersとは?Dockerで作る使い捨てテスト環境と2.0系の移行点)。
1人で開発しているなら、ハイブリッドにする必要はありません。単一ブランチで順に作成するなら、最初から-sで連番を振る方法で十分です。ただし、1人でも複数ブランチで並行作業すれば番号が重複することがあります。
本番運用:embedで単一バイナリ化とProvider APIのロック
embed.FS・goose.SetBaseFSによるマイグレーションの同梱
アプリケーションの起動時にマイグレーションを適用する場合、SQLファイルをembed.FSでバイナリに埋め込めば、コンテナにmigrationsディレクトリを置かずに済みます。
// migrations/embed.go
package migrations
import "embed"
//go:embed *.sql
var FS embed.FS
package main
import (
"context"
"database/sql"
"log"
"github.com/pressly/goose/v3"
_ "modernc.org/sqlite"
"example.com/gapp/migrations"
)
func main() {
db, err := sql.Open("sqlite", "app.db")
if err != nil {
log.Fatal(err)
}
defer db.Close()
goose.SetBaseFS(migrations.FS)
if err := goose.SetDialect("sqlite3"); err != nil {
log.Fatal(err)
}
if err := goose.UpContext(context.Background(), db, "."); err != nil {
log.Fatal(err)
}
}
migrationsパッケージをimportした時点で、同じパッケージのGoマイグレーションのinitも走って登録されます。SQLとGoの両方が1つのバイナリで適用されました。なお、createやfixのようにファイルを書き換える操作は埋め込みFSではなくOSのファイルシステムに対して動きます。
複数Pod同時起動の競合とSessionLocker
レプリカが2つ以上あるアプリで起動時にupすると、同じマイグレーションを2つのプロセスが同時に実行しようとします。CLIのgooseにも、上のgoose.UpContextのようなグローバル関数にも、ロックの仕組みはありません。3秒かかるマイグレーション(SELECT pg_sleep(3);のあとにCREATE TABLE)をgoose.UpContextで2プロセス同時に流すと、PostgreSQL 18.6で3回とも片方が失敗しました。
pod-a: OK 00001_slow.sql (3.03s)
pod-b: ERROR 00001_slow.sql: failed to run SQL migration: failed to execute SQL query "CREATE TABLE orders (id bigserial PRIMARY KEY, total integer NOT NULL);": ERROR: duplicate key value violates unique constraint "pg_class_relname_nsp_index" (SQLSTATE 23505)
空のDBで2つを同時に起動したときは、マイグレーションより手前のgoose_db_versionテーブルの作成で同じ種類の重複エラーが出て、片方が止まりました。初回のデプロイでも起きるということです。
ロックを使えるのは、goose.NewProviderで作るProvider API(v3.16.0で導入)だけです。PostgreSQLのアドバイザリロックを使うlock.NewPostgresSessionLockerを渡します。
sessionLocker, err := lock.NewPostgresSessionLocker()
if err != nil {
log.Fatal(err)
}
provider, err := goose.NewProvider(
goose.DialectPostgres,
db,
os.DirFS("migrations"),
goose.WithSessionLocker(sessionLocker),
)
if err != nil {
log.Fatal(err)
}
results, err := provider.Up(context.Background())
if err != nil {
log.Fatal(err)
}
log.Printf("applied %d migration(s)", len(results))
同じ条件で2プロセスを同時に起動すると、先にロックを取ったpod-bがapplied 1 migration(s)、待たされたpod-aがその3秒後にapplied 0 migration(s)で正常終了しました。lockパッケージの既定では、ロックを取れなかった側は5秒間隔で60回(合計5分)再試行してから諦めます。5分を超える長いマイグレーションがあるなら、オプションで延ばしてください。
importパスはgithub.com/pressly/goose/v3/lockです。MySQL/MariaDBには、v3.28.0で追加されたテーブル方式のlock.NewMySQLTableLockerをgoose.WithLockerで渡します。WithSessionLockerとWithLockerは併用できません。
ロックを入れる前に、そもそもアプリ起動時に適用すべきかを判断してください。Kubernetesであれば、デプロイ前に1回だけ走るJobでマイグレーションを実行し、アプリ本体はprovider.HasPendingで未適用が無いことだけ確かめる構成の方が、ロック待ちでPodの起動が遅れる問題も起きません。起動時適用にロックを組み合わせるのは、Jobを用意できない小さな構成に限るのがよいでしょう。既存DBのスキーマ整理やDB製品の乗り換えを含む移行計画から相談したい場合は、一創のデータベース設計・移行支援でも対応しています。
golang-migrate・Atlasとの比較:Go向けマイグレーションツールの選び方
Goで使われるマイグレーションツールの主な選択肢は、goose・golang-migrate・Atlasの3つです。
| 観点 | goose | golang-migrate | Atlas |
|---|---|---|---|
| 最新版(2026年10月) | v3.29.0 | v4.20.1 | v1.3.0 |
| ファイル構成 | 1ファイルにUp/Down | .up.sqlと.down.sqlの2ファイル | スキーマ差分から生成 |
| Goの関数で書く | できる | できない | できない |
| 失敗時の状態 | 対応する操作のみ巻き戻し | dirtyになりforceが必要 | atlas_schema_revisionsに記録 |
golang-migrateは、途中で失敗したマイグレーションがあるとDBを「dirty」と記録し、force VERSIONで手動で状態を直すまで先へ進めません(golang-migrateのFAQ)。gooseは既定で各SQLファイルをトランザクション内で実行しますが、失敗時に巻き戻せるのは、そのDBでトランザクションの対象になる操作だけです。MySQLのCREATE TABLEなど、暗黙のコミットを伴うDDLは巻き戻せません。NO TRANSACTIONを付けたファイルも巻き戻しの対象外です。
選び方は次のとおりです。手書きのSQLに加えて既存データの加工をGoで書く場面があるなら、goose。SQLファイルだけで完結し、S3やGitHubなど多様な置き場所からマイグレーションを読みたいならgolang-migrate。スキーマ定義から差分SQLを自動生成したいならAtlasです。Atlasはgoose形式のマイグレーションディレクトリを読み書きできるので(Atlasのgoose連携ガイド)、生成はAtlas、適用はgooseという分担もできます。生成時はatlas migrate diffのディレクトリURLに?format=gooseを指定します(Atlasのmigrate diffの説明)。Java(Spring Boot)の標準的な選択肢はFlywayとは?Spring Bootのマイグレーション設定・Callbacks・Spring Boot 4の変更点、PHPのフレームワーク内蔵の仕組みはLaravelマイグレーションの使い方|php artisan migrateとrollback・freshの挙動を13.xで検証で扱っています。
よくある質問
gooseとは何ですか?
Go言語で書かれたデータベースマイグレーションツール(pressly/goose)です。テーブルの作成やカラムの追加といったスキーマ変更をSQLファイルかGoの関数で書き、goose upで順番に適用し、goose downで戻します。適用履歴はDB内のgoose_db_versionテーブルに記録されます。CLIとしてもGoのライブラリとしても使え、PostgreSQL・MySQL・SQLiteなど12種類のドライバに対応しています。同じ名前のRust製負荷テストツールやAIエージェントとは別物です。
goose migrationで「found 1 missing migrations」と出たらどうすればよいですか?
適用済みの最新バージョンより古い番号のファイルが後から追加されたときのエラーです。別ブランチで作ったタイムスタンプのファイルをマージすると起きます。その場限りで適用するなら-allow-missingを付けますが、実行順がファイル名の順と変わるため本番での常用は避けます。新規の運用では、開発中はタイムスタンプ、マージ時にCIでgoose fixを実行して連番にする方法があります。ただし、既にDBへ適用したファイルは改名せず、既存の履歴と整合する番号を維持してください。
goose upとup-by-one・up-toの違いは何ですか?
goose upは未適用のマイグレーションをすべて適用します。up-by-oneは次の1つだけ、up-to 20260901000000は指定したバージョンまでを適用します。複数のマイグレーションを本番に出すとき、1つずつ適用して各段階でアプリの動作を確認したい場合はup-by-oneを使います。適用結果はgoose statusで確認できます。
go installで入れたgooseのバージョン確認と固定はどうしますか?
goose --versionでgoose version: v3.29.0のように表示されます。goose version(ハイフン無し)はDBの現在のマイグレーション番号を表示するコマンドなので混同しないでください。版を固定するにはgo install github.com/pressly/goose/v3/cmd/[email protected]のようにタグを指定します。v3.28.0以降はGo 1.26以上が必要で、Go 1.25.7以上の1.25系環境では@v3.27.3を指定します。
golang-migrateとgooseはどちらを選ぶべきですか?
既存データの加工をGoのコードで書く必要があるならgooseです。golang-migrateはSQL(または各DBのクエリ)のファイルしか扱えません。失敗時の扱いも違い、golang-migrateはDBをdirty状態にしてforceでの手動復旧を求めますが、gooseは既定でトランザクションを使います。ただし、NO TRANSACTIONを指定したファイルや、MySQLで暗黙のコミットを伴うDDLなどは巻き戻しを保証できません。SQLファイルだけで足り、S3などからマイグレーションを読みたい場合はgolang-migrateが合います。