Alembic 入門:migrations/ の中身とリビジョンの仕組みを理解する
こんにちは 人材育成室 育成メンバーチームで 研修中の はすと です。
個人的に参加しているプロジェクトのバックエンドが Python の Flask + SQLAlchemy で組まれており、データベースのマイグレーションには Alembic というツールが使われています。しかし、Alembic もマイグレーションすら触ったことがなかったので、この機会に入門してみることにしました。
まず、本記事では、「マイグレーションとは何か」を整理した上で、Alembic の基本概念と、実際に手元で動かしながら migrations/ ディレクトリの中身がどうなっているのかを確認していきます。
Flask とは
Flask は、Python の軽量な Web フレームワークです。
- ルーティング、リクエスト/レスポンス処理、テンプレートエンジン(Jinja2)など、Web アプリの最小限の機能を提供
- DB 接続・認証・フォームなどの追加機能は、必要に応じて拡張ライブラリを組み合わせて使う
- フルスタックの Django に対して、コアが小さく自分で構成を組み立てやすいのが特徴
SQLAlchemy とは
SQLAlchemy は、Python の代表的な ORM(Object Relational Mapper)ライブラリです。
- データベースのテーブルを Python のクラスとして扱える
- SQL を直接書かずに、Python のオブジェクト操作で DB にアクセスできる
- PostgreSQL / MySQL / SQLite など複数の DB に対応
- 低レベルな SQL ツールキット(Core)と高レベルな ORM の2層構造
今回紹介する Alembic は、この SQLAlchemy の作者が開発しているツールです。
マイグレーションとは
データベースのスキーマ(テーブル定義やカラムの構成)の変更や、データの初期投入・移行などを、変更履歴付きでコードとして管理する仕組みです。
アプリケーションのコードは Git で履歴を管理しますが、データベースのスキーマも同じように履歴を管理したくなる場面があります。たとえば次のようなケースです。
- 開発メンバー全員のローカル DB を同じスキーマに揃えたい
- 本番環境にカラム追加を反映するときに、誰がいつ何を変えたのか追跡したい
- リリース後に問題が起きたら、1つ前の状態に戻したい
マイグレーションの仕組みがないとどうなるか
たとえば users テーブルに age カラムを足したいときに、ツールなしで手で SQL を運用するとどうなるか、いくつかのパターンで見てみます。
1. 環境間でズレる
メンバーAがローカルで以下を叩きます。
ALTER TABLE users ADD COLUMN age INT;
同じ SQL を本番や他メンバーの環境に適用し忘れると、アプリ側で user.age を読んだ瞬間に適用していない環境だけエラーになります。
2. 二重適用してしまう
「足したっけ?」と分からなくなって、もう一度同じ SQL を叩くと、
ALTER TABLE users ADD COLUMN age INT;
-- ERROR: column "age" of relation "users" already exists
逆に適用し忘れると、その環境だけスキーマがズレたまま放置されます。
3. 順序を間違える
カラム追加と制約追加を別ファイルで管理していて、後の方を先に適用してしまうと、
-- 先に適用されてしまう
ALTER TABLE users ALTER COLUMN age SET NOT NULL;
-- ERROR: column "age" of relation "users" does not exist
依存している前の SQL がない状態で動くので、エラーになります。
4. 戻し方が手作業になる
問題が起きて戻したいときに、
-- 進めるとき
ALTER TABLE users ADD COLUMN age INT;
-- 戻すとき(自分で考える必要がある)
ALTER TABLE users DROP COLUMN age;
戻すための SQL をその場で考える必要があります。複数の変更が連続していると、戻すための SQL を作るだけで一苦労です。
マイグレーションツールはこの「どの SQL を、どの順番で、どの環境に適用したか/まだ適用していないか」を一元的に管理し、進める SQL(upgrade())と戻す SQL(downgrade())を1ファイルに記録しておけます。これによって、事故を防ぐことができます。
Alembic とは
SQLAlchemy の作者が開発している Python のマイグレーションツールです。
Alembic は SQLAlchemy プロジェクトの一部として開発されており、SQLAlchemy 製のアプリケーションでよく採用されます。今回参加しているプロジェクトでも requirements.txt に次の3つが入っていました(実際にはもっと多くのパッケージが書かれていますが、ここでは Alembic 関連の3つを抜粋しています)。
SQLAlchemy>=2.0
psycopg2-binary>=2.9
alembic>=1.13
...
それぞれの役割は次のとおりです。
SQLAlchemy: 先に紹介した ORM / SQL ツールキットpsycopg2-binary: Python から PostgreSQL に接続するためのドライバpsycopg2のバイナリ版(コンパイル不要でpip installできる)alembic: 本記事のテーマであるマイグレーションツール
requirements.txt とは
requirements.txt は、Python プロジェクトで使う外部パッケージ(ライブラリ)の一覧を記述するテキストファイルです。
- 1行に1パッケージ、
パッケージ名==バージョンやパッケージ名>=バージョンの形式で書く pip install -r requirements.txtでまとめてインストールできる- Node.js の
package.jsonや Go のgo.modと同じ役割のファイル - 最近では
pyproject.toml(Poetry / uv / Hatch などのツールが使う)という別の管理方式もある
公式ドキュメントは Alembic Documentation にあります。
プロジェクトの migrations/ を覗いてみた
実際にプロジェクトの backend/migrations/ ディレクトリを見てみると、次のような構成になっていました。
backend/
├── alembic.ini
└── migrations/
├── env.py
├── README
├── script.py.mako
└── versions/
├── 0001_initial.py
├── 0002_add_users_table.py
├── 0003_add_posts_table.py
...
└── 0047_add_comments_index.py
これは alembic init migrations コマンドを実行したときに生成されるディレクトリ構成と同じです。それぞれのファイルの役割を整理すると、次のようになっています。
| ファイル / ディレクトリ | 役割 |
|---|---|
alembic.ini |
Alembic の設定ファイル。マイグレーションスクリプトの場所、DB の接続先、ロガー設定などを書く |
migrations/env.py |
マイグレーション実行時に読み込まれる Python スクリプト。DB 接続の作り方や、対象とするメタデータをここで設定する |
migrations/script.py.mako |
新しいマイグレーションファイルを作るときのテンプレート |
migrations/versions/ |
1リビジョン1ファイルで、ここにマイグレーションスクリプトが積まれていく |
ここで「リビジョン」と呼んでいるのは、Alembic 用語でマイグレーション1つ分の単位のことです。1ファイル=1リビジョン=1回分のスキーマ変更、というイメージで読み進めてください。
versions/ の中に 47 個のファイルがあるのは、プロジェクトでこれまでに 47 回スキーマを変更してきた履歴がそのまま残っている、ということになります。
リビジョンファイルを1つ開いてみる
最も古い 0001_initial.py を開いてみます。長いので冒頭と末尾だけ抜粋します。
"""initial schema
Revision ID: 0001_initial
Revises:
Create Date: 2026-04-01 12:00:00
"""
from alembic import op
# revision identifiers, used by Alembic.
revision = '0001_initial'
down_revision = None
branch_labels = None
depends_on = None
def upgrade() -> None:
# Create enums
op.execute("CREATE TYPE post_status AS ENUM ('draft', 'published')")
# ... (テーブル作成の SQL が続く)
def downgrade() -> None:
op.execute("DROP TABLE IF EXISTS posts CASCADE")
# ... (テーブル削除の SQL が続く)
ファイルの中身を分解すると、まず押さえるべき要素は次の4つです。
| 要素 | 意味 |
|---|---|
revision |
このリビジョン自身の ID |
down_revision |
このリビジョンの1つ前のリビジョン ID。最初のファイルなので None |
upgrade() |
スキーマを「進める」ときに実行される処理 |
downgrade() |
スキーマを「戻す」ときに実行される処理 |
upgrade() と downgrade() の中で何を書くかは自由で、公式ドキュメントでは主に次のような書き方が紹介されています。
- スキーマ操作ヘルパー:
op.create_table()/op.add_column()/op.alter_column()/op.create_index()など、テーブル・カラム・インデックス・制約を直感的に操作するヘルパー関数 - 生 SQL: 今回のように
op.execute("...")で SQL 文字列を直接渡す - データ投入:
op.bulk_insert()で複数行を一気に INSERT する - 任意の SQLAlchemy 操作:
op.get_bind()で接続を取り出して、SQLAlchemy Core や ORM で複雑なデータマイグレーションを書く
リビジョンがチェーンになっている
次のファイル 0002_add_users_table.py を見ると、down_revision が 0001_initial を指していました。
revision = '0002_add_users_table'
down_revision = '0001_initial'
つまり、各リビジョンは「1つ前は誰か」を down_revision で指していて、ファイル同士が片方向のチェーンとして繋がっている、ということになります(厳密にはブランチ/マージを許す構造ですが、ブランチを使わない通常運用ではチェーンとして見て問題ありません。詳細は後述のトグルで補足します)。
None ← 0001_initial ← 0002_add_users_table ← 0003_... ← ... ← 0047_add_comments_index
↑
head
矢印 ← は down_revision がどちらを参照しているかの向きで、マイグレーションの進行方向とは逆です。
ファイル名の番号順で並んでいるから順序が決まっているわけではなく、down_revision が指している矢印で順序が決まっている、という点がポイントです。また、他のリビジョンから down_revision で参照されていないリビジョンのことを head と呼びます。ただし、ブランチを使うと head が複数同時に存在することもあります。
マージリビジョンと DAG について
ここまでは「リビジョンが1本のチェーンで繋がっている」前提で説明してきましたが、厳密にはこの形を DAG(Directed Acyclic Graph、有向非巡回グラフ) と呼びます。「向きはあるけれど、ループはしない木のような構造」 というイメージです。
通常のリビジョンは down_revision で1つの親を指します。
None ← 0001 ← 0002 ← 0003 ← 0004 (head)
ところが、複数の開発者が並行して別ブランチでマイグレーションを追加すると、head が複数同時に存在することがあります。
┌── 0002a ← 0003a (head1)
None ← 0001 ─────┤
└── 0002b ← 0003b (head2)
これを1つに戻すために作るのが マージリビジョン です。マージリビジョンの down_revision は1つではなく、
down_revision = ('0003a', '0003b')
のように タプルで複数の親を指します。
┌── 0002a ← 0003a ──┐
None ← 0001 ─────┤ ├── 0004_merge (head)
└── 0002b ← 0003b ──┘
通常の運用ではここまで意識する必要はありませんが、「down_revision がタプルになっているリビジョンを見た」ら、それはマージリビジョンだと思って良いです。
ちなみにリビジョンファイルには、4要素以外に branch_labels = None と depends_on = None も自動で挿入されています。これらはまさにこの「ブランチ/マージ」を扱うための仕組みです。
branch_labels: ブランチに名前(ラベル)を付けて、alembic upgrade <ラベル>@headのように指定したいときに使うdepends_on: 「このリビジョンを適用する前に、別のリビジョンが先に適用されている必要がある」という関係を明示したいときに使う
実際に動かしてみた
Alembic の動きを掴むために、よく使うコマンドをいくつか叩いてみます。backend/ ディレクトリ(alembic.ini がある場所)で実行します。
現在のリビジョンを確認する
$ alembic current
0047_add_comments_index (head)
DB が今どのリビジョンまで適用されているかを表示します。Alembic は alembic_version という管理用のテーブルを DB の中に作って、現在のリビジョン ID をそこに記録しています。
リビジョンの履歴を表示する
$ alembic history
0046_add_post_index -> 0047_add_comments_index (head), add comments index
0045_add_user_status -> 0046_add_post_index, add post index
0044_add_categories -> 0045_add_user_status, add user status
...
<base> -> 0001_initial, initial schema
親リビジョン(down_revision で指しているもの)を辿りながら、リビジョンの一覧が表示されます。
マイグレーションを進める
$ alembic upgrade head
未適用のリビジョンを head まで一気に適用します。1個だけ進めたいときは alembic upgrade +1 のように相対指定もできます。
マイグレーションを戻す
$ alembic downgrade -1
1つ前のリビジョンまで戻します。-2 のように複数戻す場合は、適用されたのと逆の順序で各ファイルの downgrade() が呼ばれます。
まとめ
今回 Alembic に入門してみて、押さえられたのは以下でした。
- マイグレーションは「DB スキーマの変更履歴をコードとして管理する仕組み」で、適用順序と適用済みかどうかを管理してくれる
- Alembic は SQLAlchemy 製アプリで標準的に使われる Python のマイグレーションツール
migrations/versions/の各ファイルは主にrevision/down_revision/upgrade()/downgrade()の4要素で構成されている- ファイルは番号順ではなく
down_revisionの矢印で順序が決まっていて、最終地点がheadになる alembic upgrade headで未適用のリビジョンを最後まで適用、alembic downgrade -1で1つ戻すことができる
migrations/ の中で何が起きているのかが分かると、新しくカラムを追加する PR を読む際に、versions/ に追加されたファイルが何をしているのかを理解できるようになりました。
私と同じように初めて Alembic を使ったプロジェクトに関わる方の参考になれば幸いです。










