はじめに
本記事は個人開発でGo言語を使用してAPI開発をした際のコミットした内容に基づいて生成AIから文章を生成した内容を一部書き加えたものになります
MigrationとSeeed、仲良くやるコツをサクッと整理します
TL;DR
- 役割分担: Migrationはスキーマ+最低限の必須データ、Seedは環境初期化やサンプル
- 再実行性: Seedは必ずidempotent(何度でも安全)
- up/downの原則: upは「足す」、downは「upで足した分だけ消す」
- 順序大事: 親テーブル → 子テーブル → 必須データ → (任意)Seed
- 環境分離: サンプルは本番に混ぜない dev用Seedを分ける
Seed と Migration の線引き
- Migration 行き: そのバージョンに必須の最小データ(例: 初期ロール、設定フラグ、管理者1件)
- Seed 行き: デモ/サンプル/検証用や環境依存(dev/stg のみ流す)
- 迷ったら: 「本番で必須?」と「再実行して壊れない?」の2軸で判定
基本ルール
- 再実行OK:
ON CONFLICT DO NOTHING
またはWHERE NOT EXISTS (...)
- 限定的down: upで入れた行を一意に特定してDELETE
- 環境分離:
db/seeds/dev/*
などに分け、本番はSeedを流さない - トランザクション: 1ファイル=1トランザクション(ツール依存、推奨)
SQLスニペット
- 必須データをMigrationで入れる(存在チェック)
INSERT INTO example_records (example_key, happened_at)
SELECT 'ex-001','2024-07-01 18:30:00'
WHERE NOT EXISTS (
SELECT 1 FROM example_records
WHERE example_key='ex-001'
AND happened_at='2024-07-01 18:30:00'
);
- ロールバック(upの分だけ消す)
DELTE FROM ecample_records
WHERE example_key = 'ex-001'
AND happened_at = '2024-07-01 18:30:00';
- 開発用 Seed(PostgreSQLの UPSERT で再実行OK)
先に一意制約を作ること
-- 初回に一意制約(またはユニークインデックス)
CREATE UNIQUE INDEX IF NOT EXISTS ux_example_records_key_at
ON example_records(example_key, happened_at);
-- 開発データの投入(競合時は何もしない)
INSERT INTO example_records(example_key, happened_at)
VALUES ('dev-001','2024-07-02 18:45:00')
ON CONFLICT (example_key, happened_at) DO NOTHING;
EXISTSの SELECT 1
って何?
- 意味: EXISTS/NOT EXISTSは「行の有無」だけを見る 列値は評価に使わないため
SELECT 1
でもSELECT *
でも同じ - 動作: 条件に合う行を1件でも見つけた地点で真/偽が決まる(短絡評価)
- 余談: 多くのRDBMSで実行結果は同等 読みやすさ優先でOK
Migrationの流し方
ここではgolang-migrateを想定 マイグレーションはdb/migrations
配下に置く
前提 DSN
- Docker ネットワーク越し:
postgres://postgres:password@db:5432/example_records?sslmode=disbale
- ローカル接続:
postgres://postgres:password@localhost:5432/example_records?sslmode=disable
DBの設定(docker-compose.yaml)
docker-compose.yaml
services:
db:
image: postgres:15
ports:
- "${POSTGRES_PORT:-5432}:5432"
environment:
POSTGRES_DB: example_records
POSTGRES_USER: postgres
POSTGRES_PASSWORD: password
volumes:
- db_data:/var/lib/postgresql/data
restart: always
volumes:
db_data:
DB起動
docker compose up -d db
Docker で migrate(おすすめ)
- 全て上げる
docker run --rm \
-v "$PWD/db/migrations":/migrations \
--network $(basename "$PWD")_default \
migrate/migrate -path=/migrations \
-database "postgres://postgres:password@db:5432/examle_records?sslmode=disable" up
- 全て戻す
docker run --rm \
-v "$PWD/db/migrations":/migrations \
--network $(basename "$PWD")_default \
migrate/migrate -path=/migrations \
-database "postgres://postgres:password@db:5432/examle_records?sslmode=disable" down
- 1本だけ上げる / 1本だけ戻す
# 進める
docker run --rm -v "$PWD/db/migrations":/migrations --network $(basename "$PWD")_default \
migrate/migrate -path=/migrations -database "postgres://postgres:password@db:5432/examle_records?sslmode=disable" up 1
# 戻す
docker run --rm -v "$PWD/db/migrations":/migrations --network $(basename "$PWD")_default \
migrate/migrate -path=/migrations -database "postgres://postgres:password@db:5432/examle_records?sslmode=disable" down 1
- バージョン確認 / goto / force
docker run --rm -v "$PWD/db/migrations":/migrations --network $(basename "$PWD")_default \
migrate/migrate -path=/migrations -database "postgres://postgres:password@db:5432/examle_records?sslmode=disable" version
docker run --rm -v "$PWD/db/migrations":/migrations --network $(basename "$PWD")_default \
migrate/migrate -path=/migrations -database "postgres://postgres:password@db:5432/examle_records?sslmode=disable" goto 20250730134221
docker run --rm -v "$PWD/db/migrations":/migrations --network $(basename "$PWD")_default \
migrate/migrate -path=/migrations -database "postgres://postgres:password@db:5432/examle_records?sslmode=disable" force <version>
- Linux なら host ネットワークで簡易実行
docker run --rm --network host \
-v "$PWD/db/migrations":/migrations \
migrate/migrate -path=/migrations \
-database "postgres://postgres:password@localhost:5432/examle_records?sslmode=disable" up
ローカル CLI(migrate を入れてる場合)
# macOS なら brew install golang-migrate
migrate -path db/migrations -database "postgres://postgres:password@localhost:5432/examle_records?sslmode=disable" up
migrate -path db/migrations -database "postgres://postgres:password@localhost:5432/examle_records?sslmode=disable" down 1
migrate -path db/migrations -database "postgres://postgres:password@localhost:5432/examle_records?sslmode=disable" goto 20250730134221
migrate -path db/migrations -database "postgres://postgres:password@localhost:5432/examle_records?sslmode=disable" force <version>
新しいマイグレーション作成
- Docker で生成
docker run --rm \ -v "$PWD/db/migrations":/migrations \ -u $(id -u):$(id -g) \ migrate/migrate create -dir /migrations -ext sql -format "20060102150405" add_example_feature
- ローカルで生成
migrate create -ext sql -dir db/migrations -format "20060102150405" add_example_feature
- 生成物
YYYYMMDDHHMMSS_add_example_feature.up.sql
YYYYMMDDHHMMSS_add_example_feature.down.sql
Seed の流し方(本番は非推奨、dev だけ)
- Docker の psql で実行
docker run --rm -i \ --network $(basename "$PWD")_default \ -v "$PWD/db/seeds/dev":/seeds \ postgres:15 psql "postgres://postgres:password@db:5432/examle_records?sslmode=disable" \ -f /seeds/001_example_seed.sql
- ローカル psql で実行
psql "postgres://postgres:password@localhost:5432/examle_records?sslmode=disable" \ -f db/seeds/dev/001_example_seed.sql
よくある落とし穴
- up/down の逆転: up で DELETE、down で INSERT は逆
- 重複発生: 一意キーが無いまま INSERT を繰り返す
- 依存逆順: 親より先に子テーブル作成や FK 付与
- 本番汚染: サンプルを本番 Migration に混ぜる
まとめ
- Migration は「歴史(スキーマ+最小限の必須データ)」、Seed は「初期化スクリプト(何度でもOK)」
- 重複防止は DB の一意制約と UPSERT で堅く。
WHERE NOT EXISTS
は補助的に - 迷ったら「本番に必要?」と「再実行しても壊れない?」の 2 軸で切り分け