0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

42日でバックエンドエンジニアの基礎を完全に理解する #13 - マイグレーションとDB運用

0
Posted at

この記事で学ぶこと

このシリーズではここまで、CREATE TABLEALTER TABLE も psql で直接打ってきました。#12 の準備でも、DROP してから generate_series で作り直すという豪快なことをしています。練習環境ならそれで困りません。壊れたら作り直せばいいからです。でも本番のDBには、ユーザーの本物のデータが乗っています。作り直しはできず、「手で打ったSQLを間違えた」が即障害になります。

今回は、スキーマ変更をコードと同じように履歴管理する マイグレーション を扱います。ツールの使い方だけでなく、動いているアプリを壊さずにスキーマを変える手順と、「戻せる変更・戻せない変更」の見極めまで踏み込みます。

この記事を終えると以下ができるようになります。

  • スキーマを手作業で変えてはいけない理由と、マイグレーションが解決する問題を説明できる
  • golang-migrate でスキーマ変更を適用・巻き戻しできる
  • カラム名の変更を expand-contract(足してから消す)方式で安全に行う理由を説明できる
  • down マイグレーションの限界を知り、ロールバック戦略を判断できる

この回はGo編です。ツールとしてGo製の golang-migrate を使いますが、考え方自体は言語を問いません。Python編では同じ内容を Alembic で扱う予定です。

見出しの 🔴🟡⚪ は学習の優先度です。🔴は確実に覚える、🟡は理屈を理解する、⚪は今は流してOK。迷ったら🔴だけ確実に押さえてください。


🔴 スキーマを手作業で変えてはいけない

「開発環境の psql で ALTER TABLE を打ち、うまくいったら本番でも同じSQLを打つ」という運用を想像してください。最初は回ります。でもテーブルが増え、人が増えると、必ずこうなります。

  • 本番にだけ打ち忘れたSQLがあり、開発では動くコードが本番で落ちる
  • 「この deleted_at カラム、いつ誰が何のために足したの?」に誰も答えられない
  • 新メンバーの環境構築が「この手順書のSQLを上から30個打ってください」になり、手順書はたいてい古い

冷静に考えるとちぐはぐな状態です。コードは全員がGitで管理し、いつ誰がなぜ変えたか追えるのに、そのコードが依存するDBスキーマだけは口伝と手作業。コードとスキーマは常にセットで動くのに、片方だけ履歴がありません。

ではスキーマもGitで管理しよう、となったとき、コードと決定的に違う点がひとつあります。コードは丸ごと置き換えられますが、DBは置き換えられません。 データが乗っているからです。人が住んでいる家と同じで、間取りが気に入らないからといって更地にして建て直すわけにはいきません。住人(データ)を住まわせたまま、増改築(ALTER TABLE)で変えていくしかないのです。

だからスキーマは「完成形の設計図」ではなく、変更手順の積み重ねとして管理します。これがマイグレーションです。

  • スキーマ変更を1件ずつ、番号付きのSQLファイルとしてリポジトリに入れる
  • どの環境のDBにも「何番まで適用したか」を記録し、足りない分だけ順に流す

こうすると、開発・ステージング・本番のどのDBも「同じ手順を同じ順で通った状態」になり、環境差異が原因のバグが消えます。新しい環境も、マイグレーションを頭から流すだけで再現できます。


🟡 マイグレーションの仕組み

ツールごとの差はありますが、骨組みはどれも同じで、拍子抜けするほど単純です。

1つの変更は up / down のペアで書きます。up が「進める」SQL、down が「その変更だけを打ち消す」SQLです。

db/migrations/
├── 000001_create_users.up.sql     # users テーブルを作る
├── 000001_create_users.down.sql   # users テーブルを消す
├── 000002_create_posts.up.sql
└── 000002_create_posts.down.sql

そしてツールは、DBの中に管理用の小さなテーブル(golang-migrate なら schema_migrations)を作り、そのDBが今何番まで適用済みかを記録します。増改築でいえば、番号付きファイルが工事の設計図、schema_migrations が「どこまで工事したか」の施工記録です。

適用コマンドを実行したときの動きはこれだけです。

  1. DBに記録された版数を読む(例: 2)
  2. 手元のファイルの最新版数と比べる(例: 5)
  3. 足りない 3, 4, 5 の up を番号順に流し、記録を 5 に更新する

すでに最新なら何もしません。だから何度実行しても結果は同じで、「二重に適用して壊れる」ことがありません。この性質のおかげで、デプロイのたびに機械的に流せます。


準備

PostgreSQLコンテナを起動します。

docker start pg-practice

これまでの回で作ったテーブルが残っていると話がややこしいので、いったん片付けます。psql に入って実行してください。

docker exec -it pg-practice psql -U postgres -d testdb
DROP TABLE IF EXISTS posts;
DROP TABLE IF EXISTS users;
DROP TABLE IF EXISTS accounts;

\q で抜けたら、golang-migrate のCLIをインストールします。Go圏で最も広く使われているマイグレーションツールのひとつです。

go install -tags 'postgres' github.com/golang-migrate/migrate/v4/cmd/migrate@latest
export PATH=$PATH:$(go env GOPATH)/bin

-tags 'postgres' は「PostgreSQL用のドライバを組み込んでビルドする」という指定です。migrate は多数のDBに対応していて、使うものだけ組み込む方式になっています。初回は依存のダウンロードで数分かかることがあります。2行目は go install の置き場(~/go/bin)にパスを通すためのもので、通していない場合だけ必要です。

作業ディレクトリを作っておきます。

mkdir migration-practice && cd migration-practice
mkdir -p db/migrations

db/migrations/ がマイグレーションファイルの置き場です。実務ではこのディレクトリごとGitにコミットします。


【手を動かす①】マイグレーションを作って適用する

users テーブルを作る最初のマイグレーションを作成します。

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

(実際は絶対パスで表示されます)

-seq は連番方式(000001, 000002, ...)でファイルを作る指定です。中身は空なので、SQLを書き込みます。

db/migrations/000001_create_users.up.sql

CREATE TABLE users (
    id         SERIAL PRIMARY KEY,
    name       TEXT NOT NULL,
    email      TEXT NOT NULL UNIQUE,
    created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

db/migrations/000001_create_users.down.sql

DROP TABLE users;

down には「up でやったことだけを打ち消すSQL」を書きます。テーブルを作ったなら、消す。続けて posts テーブルの分も作ります。

migrate create -ext sql -dir db/migrations -seq create_posts

db/migrations/000002_create_posts.up.sql

CREATE TABLE posts (
    id      SERIAL PRIMARY KEY,
    user_id INTEGER NOT NULL REFERENCES users(id),
    title   TEXT NOT NULL
);

CREATE INDEX idx_posts_user_id ON posts (user_id);

db/migrations/000002_create_posts.down.sql

DROP TABLE posts;

適用します。接続先の指定を毎回書くのは長いので、変数に入れておきます。

export DB_URL='postgres://postgres:postgres@localhost:5432/testdb?sslmode=disable'
migrate -path db/migrations -database "$DB_URL" up
1/u create_users (23.756649ms)
2/u create_posts (40.781511ms)

1/u は「バージョン1の up を適用した」という意味です。DBの中を見てみます。

docker exec -it pg-practice psql -U postgres -d testdb -c '\dt'
               List of relations
 Schema |       Name        | Type  |  Owner
--------+-------------------+-------+----------
 public | posts             | table | postgres
 public | schema_migrations | table | postgres
 public | users             | table | postgres
(3 rows)

作った覚えのない schema_migrations がいます。これがさっき説明した施工記録です。

docker exec -it pg-practice psql -U postgres -d testdb -c 'SELECT * FROM schema_migrations;'
 version | dirty
---------+-------
       2 | f

「このDBはバージョン2まで適用済み」という記録だけが入っています。この状態でもう一度 up を流すと、

migrate -path db/migrations -database "$DB_URL" up
no change

何も起きません。記録が2で、ファイルの最新も2だからです。これが「何度流しても安全」の実体です。

down も試します。数を付けると、その数だけ巻き戻します。

migrate -path db/migrations -database "$DB_URL" down 1
2/d create_posts (13.753674ms)

バージョン2の down、つまり DROP TABLE posts が実行され、記録は1に戻ります。\dt を見ると posts が消えているはずです。もう一度 up を流せば 2 に戻ります。ファイルさえ揃っていれば、スキーマを好きな版に行き来できるわけです。

🟡 失敗すると「dirty」で止まる

マイグレーションのSQLが失敗するとどうなるかも、先に見ておきます。実務で初見だと慌てるポイントです。わざと構文を間違えたマイグレーションを作ります。

migrate create -ext sql -dir db/migrations -seq add_users_bio

db/migrations/000003_add_users_bio.up.sql(TABLE を TABEL と打ち間違えた想定です)

ALTER TABEL users ADD COLUMN bio TEXT;

db/migrations/000003_add_users_bio.down.sql

ALTER TABLE users DROP COLUMN bio;

適用すると当然失敗します。

migrate -path db/migrations -database "$DB_URL" up
error: migration failed: syntax error at or near "TABEL" (column 7) in line 1: ALTER TABEL users ADD COLUMN bio TEXT;
 (details: pq: syntax error at or near "TABEL")

ここからが本題で、この後にもう一度 up を流すと、エラーの内容が変わります。

error: Dirty database version 3. Fix and force version.

schema_migrations を見ると version = 3, dirty = t になっています。dirty は「バージョン3を適用しようとして、途中で死んだ」という印です。

なぜわざわざ止まるのか。マイグレーションには複数のSQL文を書けるので、失敗した時点でどこまで実行されたかツールには分からないからです。中途半端な状態の上に次を流せば、被害が広がります。だから「人間が確認して直すまで動かない」が正しい挙動です。

直し方は2段階です。まず、失敗したSQLがDBに何を残したか確認し、必要なら手で片付けます(今回は構文エラーで何も実行されていないので、片付けは不要です)。次にファイルの間違いを直し、「バージョン2までは正常」とツールに教えてから、改めて適用します。

# 000003_add_users_bio.up.sql の TABEL を TABLE に直してから
migrate -path db/migrations -database "$DB_URL" force 2
migrate -path db/migrations -database "$DB_URL" up
3/u add_users_bio (7.920651ms)

force 2 は「記録をバージョン2・dirtyなしに書き換えろ」という命令です。DBの実態と記録を人間が突き合わせた上で使う、いわば非常ボタンなので、原因を確認する前に押してはいけません。


【手を動かす②】動いているアプリを壊さずに変える

ここからが今回の核心です。マイグレーションで履歴管理ができても、変更の中身が危険なら事故は起きます。実際に事故を再現します。

動いているアプリ役として、users を読むだけの小さなプログラムを用意します。まずデータを入れます。

docker exec -it pg-practice psql -U postgres -d testdb -c \
  "INSERT INTO users (name, email) VALUES ('佐藤', 'sato@example.com'), ('鈴木', 'suzuki@example.com');"

app.go を作ります。

package main

import (
	"database/sql"
	"fmt"
	"log"

	_ "github.com/lib/pq"
)

func main() {
	db, err := sql.Open("postgres",
		"host=localhost port=5432 user=postgres password=postgres dbname=testdb sslmode=disable")
	if err != nil {
		log.Fatal("接続設定に失敗:", err)
	}
	defer db.Close()

	rows, err := db.Query("SELECT id, name FROM users ORDER BY id")
	if err != nil {
		log.Fatal("ユーザーの取得に失敗: ", err)
	}
	defer rows.Close()

	for rows.Next() {
		var id int
		var name string
		if err := rows.Scan(&id, &name); err != nil {
			log.Fatal(err)
		}
		fmt.Printf("id=%d name=%s\n", id, name)
	}
	if err := rows.Err(); err != nil {
		log.Fatal(err)
	}
}
go mod init migration-practice
go get github.com/lib/pq
go run app.go
id=1 name=佐藤
id=2 name=鈴木

このアプリが本番で動いている、という設定です。ここで「name は姓名どちらか分かりにくいから full_name に改名したい」という要望が来たとします。素直にやるとこうです。

migrate create -ext sql -dir db/migrations -seq rename_users_name

db/migrations/000004_rename_users_name.up.sql

ALTER TABLE users RENAME COLUMN name TO full_name;

db/migrations/000004_rename_users_name.down.sql

ALTER TABLE users RENAME COLUMN full_name TO name;

適用して、動いているアプリ(さっきの app.go のまま)を実行すると、

migrate -path db/migrations -database "$DB_URL" up
go run app.go
2026/08/21 10:22:36 ユーザーの取得に失敗: pq: column "name" does not exist at column 12 (42703)
exit status 1

落ちました。マイグレーションは成功、SQLも正しい、それでも障害です。

🔴 なぜ一発のRENAMEが事故になるのか

「アプリのコードも full_name に書き換えてから一緒にデプロイすればいいのでは」と思うかもしれません。それでも事故になります。理由は、マイグレーションの適用とアプリの入れ替わりが同時ではないからです。

デプロイの現場では、マイグレーションを流してから新しいアプリが起動しきるまでに、短くても数秒、複数台のサーバーに順番に配る構成なら数分の間があります。その間、旧コードが新スキーマの上で動く時間が必ず生まれ、そこにいるユーザーのリクエストは全部いま見たエラーで落ちます。逆順(アプリを先に入れ替える)にしても、今度は新コードが旧スキーマを読んで同じことが起きます。

つまり覚えるべきルールはこうです。稼働中のコードが使っている列やテーブルを、いきなり消したり改名したりしない。 RENAME は「消す + 作る」を同時にやる操作なので、いきなりやる限り必ずどちらかのコードが割を食います。

今回はローカルの練習なので、down で戻して、この失敗作のマイグレーションファイルは削除してしまいます。

migrate -path db/migrations -database "$DB_URL" down 1
rm db/migrations/000004_rename_users_name.*.sql

ファイルを消してよいのは、まだ自分のローカルにしかないからです。チームに共有された(他の環境に適用されたかもしれない)マイグレーションを消したり書き換えたりすると、環境ごとに通った歴史が食い違って収拾がつかなくなります。共有後に直したければ、新しい番号のマイグレーションで直します。

🔴 expand-contract 足してから、消す

安全にやる方法は昔から確立されていて、expand-contract(拡張と収縮)と呼ばれます。発想は単純で、「足す」は旧コードを壊さないが「消す・変える」は壊すという非対称性を使います。RENAME という危険な1手を、安全な「足す」と、時間を置いた「消す」に分解するのです。

手順を今回の例でやってみます。まず expand、新カラムを足して既存データを埋めます。

migrate create -ext sql -dir db/migrations -seq add_users_full_name

db/migrations/000004_add_users_full_name.up.sql

ALTER TABLE users ADD COLUMN full_name TEXT;

UPDATE users SET full_name = name;

db/migrations/000004_add_users_full_name.down.sql

ALTER TABLE users DROP COLUMN full_name;
migrate -path db/migrations -database "$DB_URL" up
go run app.go
id=1 name=佐藤
id=2 name=鈴木

マイグレーション適用後も、旧コードのアプリがそのまま動いています。カラムを足しただけなので、name を読むコードには何の影響もないからです。DBの中は両方のカラムが埋まった状態になっています。

 id | name | full_name
----+------+-----------
  1 | 佐藤 | 佐藤
  2 | 鈴木 | 鈴木

次に、アプリのコードを full_name を使うように書き換えてデプロイします(app.go のSQLを SELECT id, full_name FROM users に変えるだけなので、手元でも試せます)。この移行期間の注意点がひとつだけあります。旧コードは新規登録時に name にしか書き込まないので、移行期間中に増えた行は full_name が空になり得ます。実務では「新コードは両方のカラムに書く」期間を挟み、全台が切り替わった後にもう一度埋め直しの UPDATE を流して取りこぼしを消します。

全部のコードが full_name だけを使うようになったのを確認してから、最後に contract、古いカラムを消します。

migrate create -ext sql -dir db/migrations -seq drop_users_name

db/migrations/000005_drop_users_name.up.sql

ALTER TABLE users DROP COLUMN name;

db/migrations/000005_drop_users_name.down.sql

ALTER TABLE users ADD COLUMN name TEXT;
migrate -path db/migrations -database "$DB_URL" up
5/u drop_users_name (10.410533ms)

これで改名が完了しました。手数は増えましたが、どの瞬間を切り取っても「その時点で動いているコード」と「その時点のスキーマ」が両立しています。デプロイに何分かかろうと、エラーになる時間帯がありません。

🔴 down ではデータは戻らない

ところで、いま作った 000005 の down は本当に「打ち消し」になっているでしょうか。試しに巻き戻してみます。

migrate -path db/migrations -database "$DB_URL" down 1
docker exec -it pg-practice psql -U postgres -d testdb -c \
  "SELECT id, name, full_name FROM users ORDER BY id;"
 id | name | full_name
----+------+-----------
  1 |      | 佐藤
  2 |      | 鈴木

name カラムは戻ってきましたが、中身は空です。当然で、DROP COLUMN した瞬間にデータは消えており、down の ADD COLUMN は器を作り直すことしかできません。スキーマは戻せても、データは戻らない。 これが down マイグレーションの限界です。

この限界があるので、実務のロールバック戦略は次のように考えます。

  • down は開発中の道具と割り切る。ローカルで作っては壊すときには便利に使う
  • 本番で問題が起きたら、巻き戻すのではなく直す変更を新しいマイグレーションとして前に進める(roll forward と呼びます)
  • それでも書く。down を書く過程で「この変更は戻せない種類か」に必ず気づけるからです。DROP を含む変更の down が書けないと分かった時点で、慎重になれます

「戻せない」が前提になると、さっきの expand-contract の価値も一段はっきりします。contract(削除)を焦らず十分に時間を置いてから行うのは、それが取り返しのつかない一手だからです。私も手元で DROP COLUMN から down を流して空のカラムが返ってきたのを見たとき、この操作の片道感が一番腹に落ちました。ぜひ自分の環境で見ておいてください。


実務での判断基準

🔴 1マイグレーション1目的

「users にカラム追加」と「posts にインデックス追加」を1ファイルにまとめない、が原則です。理由は失敗時の切り分けです。途中で死んだとき、1ファイルに5個の変更が入っていると「どこまで実行されたか」の確認範囲が5倍になります。dirty からの復旧手順で見たとおり、この確認は人間がやるしかありません。変更が小さいほど、事故対応が単純になります。

🔴 共有済みのマイグレーションは書き換えない

一度 push したマイグレーションは、もう誰かの環境に適用されたかもしれません。その前提で、直したいことがあっても既存ファイルには触らず、新しい番号のマイグレーションとして追加します。履歴を書き換えないという点で、push 済みの Git コミットを rebase しない、と同じ規律です。

🟡 NOT NULL カラムの追加は既存の行を考える

既存データのあるテーブルに NOT NULL カラムを素朴に足すと、失敗します。

ALTER TABLE users ADD COLUMN status TEXT NOT NULL;
ERROR:  column "status" of relation "users" contains null values

既存の行にとって新カラムの値は NULL なので、NOT NULL 制約と矛盾するからです。DEFAULT を付ければ、既存の行はデフォルト値で埋まるので通ります。

ALTER TABLE users ADD COLUMN status TEXT NOT NULL DEFAULT 'active';

「制約はスキーマだけの問題ではなく、いま入っているデータとの整合の問題」という視点を持ってください。同じ理由で、UNIQUE 制約の追加も既存データに重複があれば失敗します。

⚪ 大きいテーブルへのインデックス追加はロックに注意

CREATE INDEX は、実行中そのテーブルへの書き込みを止めます。#8 で使った10万件程度なら一瞬ですが、数千万行の本番テーブルだと分単位で書き込みが止まり、それは実質的に障害です。PostgreSQL には書き込みを止めずにインデックスを作る CREATE INDEX CONCURRENTLY があります。

ひとつ罠があって、CONCURRENTLY はトランザクションの中で実行できません。マイグレーションファイルに複数の文を書くと全体がひとつのトランザクションとして実行されるため、他の文と同居させるとこのエラーで dirty になります。

error: migration failed: CREATE INDEX CONCURRENTLY cannot run inside a transaction block

CONCURRENTLY を使うときは、そのマイグレーションファイルにその1文だけを書きます。今は「大きいテーブルにインデックスを足すときは調べることがある」とだけ覚えておけば十分です。

🟡 適用は人手ではなく仕組みに任せる

マイグレーションの適用をデプロイの一部として自動実行する(CIや起動スクリプトで migrate up を流す)のが実務の定石です。手作業を排除したくてマイグレーションを導入したのに、適用だけ手作業では片手落ちだからです。「何度流しても安全」という冪等性は、まさにこの自動化のためにあります。


確認テスト

知識問題

Q1. スキーマを psql の手作業で変更する運用には、どんな問題がありますか。2つ以上挙げ、マイグレーションがそれをどう解決するか説明してください。

Q2. schema_migrations テーブルは何を記録していて、何のためにありますか。dirty が立つのはどんなときで、なぜツールはそこで止まりますか。

Q3. 稼働中のサービスで users テーブルの name カラムを full_name に改名します。RENAME COLUMN を1回流すだけの方法がダメな理由と、正しい手順を説明してください。

Q4. DROP COLUMN を含むマイグレーションを down で巻き戻すと、何がどこまで戻りますか。それを踏まえて、本番障害時に down ではなく roll forward が選ばれる理由を説明してください。

実技問題

Q5. posts テーブル(既存データあり)に、公開フラグ published(BOOLEAN、NOT NULL)を追加するマイグレーションを書いてください。以下を満たすこと。

  • 既存の行があってもエラーにならないこと(既存の投稿は非公開扱いとする)
  • up / down の両方を書くこと
  • この down で失われるものがあるか、一言添えること

確認テスト 解答・解説

Q1

代表的な問題は次のとおりです。

  1. 環境差異が生まれる。開発・ステージング・本番のどれかで打ち忘れや打ち間違いがあっても、気づく仕組みがない
  2. 履歴が残らない。いつ誰がなぜ変えたか追えず、「このカラムは何?」に答えられなくなる
  3. 再現できない。新しい環境をゼロから作る確実な手順が存在しなくなる

マイグレーションは、変更を番号付きのSQLファイルとしてリポジトリに入れることで履歴を残し(2の解決)、各DBに適用済み版数を記録して足りない分だけを順に流すことで、どの環境も同じ手順を通った状態にします(1と3の解決)。「コードと同じように、スキーマの変更もバージョン管理する」と一言で説明できれば合格です。

Q2

schema_migrationsそのDBがマイグレーションを何番まで適用済みかを記録しています。ツールはこの記録と手元のファイルを比べて差分だけを適用するので、何度実行しても安全になります。適用状態をDB自身に持たせるのは、環境ごとに進み具合が違うからです。

dirty は、あるバージョンの適用が途中で失敗したときに立ちます。マイグレーションには複数のSQL文を書けるため、失敗時点でどこまで実行されたかツールには判断できません。分からないまま次を流すと壊れ方が広がるので、人間が実態を確認して force で記録を直すまで、あえて動かない設計になっています。

Q3

ダメな理由は、マイグレーションの適用とアプリの入れ替わりの間に時間差があるからです。RENAME を流した瞬間から新コードが動き出すまでの間、旧コード(name を読む)が新スキーマの上で動き、pq: column "name" does not exist で落ち続けます。コードを先に替えても、逆向きに同じ問題が起きます。

正しい手順は expand-contract です。

  1. full_name カラムを追加し、既存データを UPDATE でコピーする(旧コードは壊れない)
  2. アプリを full_name を使うコードに更新してデプロイする。移行期間に増える行の取りこぼしは、両方に書く期間を挟むか、切り替え後の埋め直しで対処する
  3. 全コードの切り替えを確認してから、name削除する

「足すのは安全、消す・変えるのは危険。だから危険な1手を、安全な『足す』と時間を置いた『消す』に分解する」という原理まで言えれば完璧です。

Q4

戻るのはカラムという器だけです。down の ADD COLUMN でスキーマ上は元の形に見えますが、DROP COLUMN の時点でデータは消えているので、中身はすべて NULL になります(手を動かす②で実測したとおりです)。

つまり down は「スキーマの巻き戻し」であって「時間の巻き戻し」ではありません。本番で down を流しても失われたデータは戻らず、むしろ NULL だらけのカラムを正常なデータのように見せてしまう危険すらあります。だから本番では、壊れた状態を出発点として修正を新しいマイグレーションで前に進める roll forward が基本になります。失われたデータの復旧は、マイグレーションではなくバックアップの仕事です。

Q5

NNNNNN_add_posts_published.up.sql

ALTER TABLE posts ADD COLUMN published BOOLEAN NOT NULL DEFAULT false;

NNNNNN_add_posts_published.down.sql

ALTER TABLE posts DROP COLUMN published;

適用して確認した結果です。既存の行はデフォルト値で埋まります。

 id |   title    | published
----+------------+-----------
  1 | 最初の投稿 | f

ポイント

  • DEFAULT false が無いと、既存の行がある時点で column "published" of relation "posts" contains null values で失敗する。「既存の投稿は非公開扱い」という仕様を、デフォルト値そのもので表現している
  • down は DROP COLUMN。この down を流すと、追加後に各投稿へ設定した公開・非公開の値は失われる。器は戻るが中身は戻らない、をここでも意識できているかがこの問題の狙い

まとめ

  • データが乗ったDBは作り直せないので、スキーマは「完成形」ではなく「変更手順の積み重ね」で管理する。それがマイグレーションで、変更は up / down のSQLファイル、適用状況は schema_migrations がDBごとに記録する
  • 適用は差分だけが流れるので何度実行しても安全。失敗すると dirty で止まり、人間が確認して force で復旧する
  • マイグレーションの適用とアプリの入れ替わりには必ず時間差がある。稼働中のコードが使う列をいきなり消さず・改名せず、expand-contract(足す、切り替える、時間を置いて消す)に分解する
  • down で戻るのはスキーマだけで、消えたデータは戻らない。本番は roll forward が基本、データの復旧はバックアップの仕事
  • 1マイグレーション1目的。共有済みのファイルは書き換えず、直したければ新しい番号で前に進める

データベースを直接操作する話はこの回でひと区切りです。次回は #14 レイヤードアーキテクチャ① なぜ層を分けるか です。ここまで main 関数にベタ書きしてきたDB操作やHTTP処理を、変更に強い形に整理していきます。その第一歩として、そもそもなぜ層を分けるのかから考えます。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?