12 人が閲覧(直近 30 日) データベース

Alembic(アレンビック)とは?SQLAlchemyのマイグレーションの使い方とautogenerateの落とし穴【1.20対応】

Alembic(アレンビック)とは?SQLAlchemyのマイグレーションの使い方とautogenerateの落とし穴【1.20対応】

Alembic(アレンビック)は、PythonのORMであるSQLAlchemyのために作られたデータベースマイグレーションツールです。テーブルや列の変更をPythonのスクリプトとして残し、alembic upgrade で適用、alembic downgrade で巻き戻します。作者はSQLAlchemyと同じMike Bayerで、ライセンスはMITです。

2026年9月11日公開の最新版1.20.0から、SQLAlchemy 1.4のサポートが外れ、SQLAlchemy 2.0以上とPython 3.10以上が必須になりました。この記事では、1.20.0・SQLAlchemy 2.1.1・SQLite 3.53.1の環境で実際にコマンドを流した結果をもとに、基本の使い方と、自動生成(autogenerate)をそのまま適用するとデータが消える場面を解説します。なお、Sony Pictures ImageworksとIndustrial Light & Magicが公開した3DCGのシーン交換形式(拡張子 .abc)も同じ「Alembic」という名前ですが、この記事で扱うのはPythonのツールです。

まとめ:Alembicの要点

  • AlembicはSQLAlchemyのモデル定義と実際のDBを比べ、差分をマイグレーションスクリプトとして生成・適用するツール。DBの現在のリビジョンは alembic_version テーブルに記録され、複数の分岐先まで適用されている場合は複数行になる
  • 基本の流れは alembic init → env.pyに target_metadata を設定 → alembic revision --autogenerate → 生成ファイルを読む → alembic upgrade head
  • 1.20.0(2026-09-11)でSQLAlchemy 1.4対応を終了。1.4のまま運用するプロジェクトは alembic<1.20 に固定する
  • autogenerateは列名・テーブル名の変更を検出できず「追加+削除」として出力する。NULL許可の列ならエラーも出ずに値が消えるため、op.alter_column(..., new_column_name=...) に書き換える
  • SQLiteは列の型変更をALTERで実行できない(NOT NULLの変更は3.53.0から対応)。batch_alter_table を使い、失敗したら元テーブルとデータが残っていることを確かめたうえで、作業用の _alembic_tmp_ テーブルだけを削除して再実行する
  • 本番では --sql で実行予定のSQLをレビューし、CIでは alembic check(差分があると終了コード255)でモデルとマイグレーションのずれを止める

まず、SQLAlchemy本体とAlembicがそれぞれ何を担当するかを整理します。

Alembicとは:SQLAlchemy公式のマイグレーションツール

Alembicのソースコードは、GitHubのSQLAlchemy組織(sqlalchemy/alembic)で管理されています。読みは「アレンビック」で、英単語のalembicは蒸留器(ランビキ)を指します。

マイグレーションツールが解決するのは、「どのDBに、どこまでのスキーマ変更が当たっているか」をコードで管理する問題です。Alembicは変更ごとに1ファイル(リビジョン)を作り、各ファイルが直前のリビジョンID(down_revision)を持つことで変更履歴をチェーン状につなぎます。DB側には alembic_version テーブルが作られ、現在のリビジョンIDが記録されます。履歴の先端が一つなら1行、適用済みの分岐先が複数あれば複数行になります。開発機・ステージング・本番のどれでも、alembic upgrade head を流せば最新のスキーマにそろいます。

SQLAlchemy・Flask-Migrate・Djangoとの関係

SQLAlchemy本体はテーブル定義やクエリの発行を担い、既存テーブルの変更は扱いません(create_all() は存在しないテーブルを作るだけです)。その変更部分を受け持つのがAlembicです。ORM自体の書き方はSQLAlchemyの基本と使い方、ORMという仕組み全般はORMの仕組みとメリット・デメリットで解説しています。

FlaskのFlask-Migrateは、内部でAlembicを呼び出す拡張です。flask db migrate の実体はAlembicのautogenerateなので、この記事の注意点がそのまま当てはまります。SQLModelのモデルもSQLAlchemyのMetaDataを持つため、env.pyで SQLModel.metadata を渡せばAlembicで管理できます。

Djangoは独自のマイグレーション機能を持ち、Alembicは使いません。比較の基準も異なり、Djangoの makemigrations は過去のマイグレーションファイルから復元した状態とモデルを比べます。Alembicのautogenerateは、接続した実際のDBとモデルを比べます。そのため、未適用のリビジョンが残ったDBで alembic revision --autogenerate を実行すると、Target database is not up to date. で止まります。Django側の流れはDjango migrateの実務を参照してください。

Alembic 1.20の対応環境と押さえるべき変更点

PyPIの1.20.0は Requires-Python >=3.10、依存は SQLAlchemy>=2.0 とテンプレートエンジンのMakoです。直近の変更のうち、既存プロジェクトの動作に影響するものを公式の変更履歴(changelog)から抜き出しました。

版 公開日 変更点
1.20.0 2026-09-11 SQLAlchemy 1.4対応を終了(2.0が最低要件)
1.19.2 2026-09-04 名前付きCHECK制約の検出を既定で無効に戻す
1.19.0 2026-08-04 名前付きCHECK制約の追加・削除を検出
1.16.0 2025-05-21 pyproject.tomlでの設定に対応
1.12.0 2023-08-31 型変更の検出(compare_type)を既定で有効に
1.9.0 2022-12-15 alembic check コマンドを追加

1.20.0の変更履歴には、SQLAlchemy 1.4がPython 3.13以上に対応していないことが削除の理由として書かれています。まだ1.4を使っているなら、Alembicを上げる前にSQLAlchemyを2.0へ移行する必要があります。CHECK制約の検出は1.19.0で既定の動作になりましたが、誤検出のおそれがあるため1.19.2で既定から外れました。1.19.0と1.19.1だけ挙動が違う点に注意してください。

インストールとalembic initで作られるファイル

Alembicは、プロジェクトごとの仮想環境に入れるのが公式ドキュメントの推奨です。使うDBのドライバ(PostgreSQLならpsycopgなど)は別途インストールします。

python -m venv .venv
. .venv/bin/activate
pip install alembic
alembic init migrations

alembic init は、alembic.ini と migrations/ ディレクトリ(env.py・script.py.mako・versions/)を作ります。alembic list_templates で選べるテンプレートは1.20.0で5種類です。

テンプレート 用途
generic 既定。単一DB・同期ドライバ
async asyncpgなど非同期ドライバ
multidb 複数DBを1つの履歴で管理
pyproject 設定をpyproject.tomlへ置く
pyproject_async pyproject+非同期ドライバ

alembic init -t pyproject migrations を実行すると、pyproject.toml に [tool.alembic] セクションが書かれ、script_location などソースコード側の設定はそこへ移ります。ただし alembic.ini も同時に作られ、DBの接続URL(sqlalchemy.url)とログ設定はこちらに残ります。接続先は環境ごとに変わり、ソースコードの設定とは性質が違うためです。

env.pyのtarget_metadata設定

autogenerateを使うには、env.pyの target_metadata = None をモデルのMetaDataに書き換えます。モデルを読み込まないままMetaDataが空になると、AlembicはDBにあるテーブルを削除対象として検出します。検証では、既存の users テーブルに対して op.drop_table('users') が生成されました。Base.metadata を渡していても、モデルクラスを定義したモジュールをimportしていなければ同じ状態になります。target_metadata が None のままなら、--autogenerate はMetaDataが渡されていないというエラーで止まります。

# migrations/env.py
import os
from models import Base   # モデルを定義したモジュールを必ずimportする

target_metadata = Base.metadata

# 接続URLは環境変数から渡す(alembic.iniに本番の認証情報を書かない)
config.set_main_option("sqlalchemy.url", os.environ["DATABASE_URL"].replace("%", "%%"))

set_main_option はConfigParserの変数展開を通るため、URLエンコードされたパスワードに含まれる % は %% にしないとエラーになります(Alembicの config.py のdocstringに明記)。

Alembicの使い方:revision・upgrade・downgradeの流れ

次のモデルをmodels.pyに保存し、Unix系シェルで export DATABASE_URL='sqlite:///app.db' を実行してから、1本目のマイグレーションを作ります。

# models.py
from sqlalchemy import String
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column

class Base(DeclarativeBase):
    pass

class User(Base):
    __tablename__ = "users"
    id: Mapped[int] = mapped_column(primary_key=True)
    name: Mapped[str] = mapped_column(String(50))

alembic revision --autogenerate -m "create users" を実行すると、Detected added table 'users' と表示され、versions/にファイルが1つ生成されました。中身は次のとおりです。

revision: str = '04fcf8a2cc0f'
down_revision: Union[str, Sequence[str], None] = None

def upgrade() -> None:
    op.create_table('users',
    sa.Column('id', sa.Integer(), nullable=False),
    sa.Column('name', sa.String(length=50), nullable=False),
    sa.PrimaryKeyConstraint('id')
    )

def downgrade() -> None:
    op.drop_table('users')

Mapped[str] と書いた列は nullable=False(NOT NULL)になります。NULLを許可したい列は Mapped[str | None] と書きます。生成されたファイルは「候補」なので、公式ドキュメントも適用前に必ず読んで直すよう求めています。

確認できたら alembic upgrade head で適用します。主なコマンドは次のとおりです。

コマンド 動作
alembic upgrade head 最新まで適用
alembic upgrade +1 1つだけ進める
alembic downgrade -1 1つ戻す
alembic downgrade base すべて戻す
alembic current DBの現在のリビジョン
alembic history リビジョンの一覧
alembic heads 末端のリビジョン
alembic stamp head SQLを流さず版だけ記録

適用後の alembic current は 04fcf8a2cc0f (head) を返し、alembic_version テーブルにも同じIDが1行入っていました。stamp は、Alembic導入前から存在するDBに「ここまでは適用済み」と記録するときに使います。

downgrade(ロールバック)で戻る範囲

alembic downgrade -1 は、単一の履歴では、現在適用されているリビジョンの downgrade() 関数を実行し、その親リビジョンへ戻ります。戻るのはスキーマで、データは戻りません。downgrade() が drop_column を含むなら、その列の値は消えます。downgrade base を実行すると、1本目の drop_table まで流れてテーブルごと消えます。検証では、base まで戻した後も alembic_version テーブル自体は残り、中身だけが空になりました。本番で戻す可能性があるリビジョンは、downgradeの動作をステージングで一度流してから出すのが安全です。

autogenerateが検出できない変更と、データが消える場面

公式ドキュメントのautogenerateの章は、「autogenerateは完璧を意図していない」と明記し、検出できる変更と検出できない変更を分けて示しています。

変更 検出
テーブル・列の追加と削除 される
NULL制約の変更 される
インデックス・名前付きUNIQUE・外部キー される(基本的な変更)
列の型変更 される(1.12.0から既定)
サーバー側デフォルト値の変更 既定では無効
名前付きCHECK制約 既定では無効
テーブル名・列名の変更 されない(追加+削除になる)
名前の無い制約 されない

列名変更の誤検出とデータ消失

先ほどの name 列を full_name に改名し、Userモデルへ email: Mapped[str | None] = mapped_column(String(255)) も追加してautogenerateを実行すると、次の3行が生成されました。

op.add_column('users', sa.Column('full_name', sa.String(length=50), nullable=False))
op.add_column('users', sa.Column('email', sa.String(length=255), nullable=True))
op.drop_column('users', 'name')

2行入ったテーブルにこれを適用すると、SQLiteは Cannot add a NOT NULL column with default value NULL で止まりました。止まったのは、新しい列がNOT NULLだったからです。NULL許可の列で同じ改名をした場合はエラーが出ず、既存の値 taro が None に変わった状態で適用が完了しました。追加された列は空で、元の列は削除されるためです。

改名は、生成されたコードを次のように書き換えます。この形に直したところ、2行の値は full_name 列にそのまま残り、alembic check も差分なしを返しました。

def upgrade() -> None:
    op.add_column("users", sa.Column("email", sa.String(255), nullable=True))
    op.alter_column("users", "name", new_column_name="full_name")

def downgrade() -> None:
    op.drop_column("users", "email")
    op.alter_column("users", "full_name", new_column_name="name")

レビューでは、同じテーブルに add_column と drop_column が並んでいたら改名を疑ってください。autogenerateの結果を読まずに upgrade する運用は、この事故に直結します。

デフォルト値の変更と名前の無い制約

列の長さを String(50) から String(100) に変え、同時に server_default を追加して実行すると、検出されたのは型の変更だけでした。サーバー側のデフォルト値まで比較するには、env.pyの context.configure() に compare_server_default=True を渡します。公式ドキュメントは、単純なケース以外では正確にならないことがあるとして、先に対象のスキーマで試すよう求めています。

DROP CONSTRAINT には名前が必要なため、制約名が None のままでは通常の drop_constraint で削除対象を指定できません。既存の制約は、DB側で自動的に付いた名前を調べて指定するか、SQLiteならbatch modeの naming_convention 引数で名前を与えて削除します。1.20.0からは、名前が None の drop_constraint を生成すると警告コメントが入ります。MetaDataに naming_convention を設定し、制約名が必ず付くようにしておくのが根本的な対策です。

SQLiteでALTERが通らないときのbatch mode

SQLiteはALTER文の対応が限られています。列の長さを String(255) から String(500) に変える op.alter_column をそのまま適用すると、near "TYPE": syntax error で失敗しました。SQLiteには列の型を変えるALTER構文が無いためです。一方、NOT NULL制約の追加・削除(ALTER COLUMN ... SET NOT NULL)はSQLite 3.53.0(2026-04-09)で対応しました。検証環境の3.53.1では、既存のNULLを埋めた後ならbatchを使わずに通りました。Pythonの sqlite3 モジュールが使うSQLiteの版は環境によって違うため、sqlite3.sqlite_version で確認してください。

ALTERで表現できない変更に対して、Alembicは新しい構造の一時テーブルを作ってデータを写し、元のテーブルと差し替える「move and copy」方式の batch_alter_table を用意しています(公式ドキュメントのbatch migrationsの章)。次は email 列をNOT NULLにする例で、古いSQLiteでも動きます。

def upgrade() -> None:
    op.execute("UPDATE users SET email = '' WHERE email IS NULL")
    with op.batch_alter_table("users") as batch_op:
        batch_op.alter_column("email", existing_type=sa.String(255),
                              nullable=False, server_default="")

env.pyの context.configure() に render_as_batch=True を渡すと、autogenerateが最初からこの with op.batch_alter_table(...) の形で出力します。SQLiteを使うなら、この設定を入れておくのが無難です。

batchが失敗した後に残る一時テーブル

上のコードから UPDATE 行を外し、email がNULLの行が残ったまま適用すると、NOT NULL constraint failed: _alembic_tmp_users.email で失敗しました。server_default は新しく入る行の既定値で、写してくる既存行のNULLは埋めません。

問題はその後です。失敗後のDBには _alembic_tmp_users テーブルが残り、alembic_version は1つ前のリビジョンのままでした。この状態では、UPDATE を足して再実行しても table _alembic_tmp_users already exists で止まります。SQLiteでは、AlembicがDDLを非トランザクションとして扱うためです(実行ログに Will assume non-transactional DDL と出ます)。DROP TABLE _alembic_tmp_users で一時テーブルを消してから再実行すると、適用が完了し、2行とも email に空文字が入りました。

batch modeは、--sql(オフラインモード)とも相性がよくありません。SQLiteに対して --sql で出力しようとすると、テーブル構造を読み取るためにDB接続が必要だというエラーで止まります。回避するには、batch_alter_table の copy_from にTableオブジェクトを渡します。

チーム開発で起きる複数headとalembic merge

2人が同じリビジョンから別々にマイグレーションを作ると、履歴が枝分かれします。検証で同じ親から2本作ったところ、alembic heads が2行を返し、alembic upgrade head は次のエラーで止まりました。

FAILED: Multiple head revisions are present for given argument 'head';
please specify a specific target revision, '<branchname>@head' to narrow
to a specific head, or 'heads' for all heads

alembic merge heads -m "merge posts and tags" を実行すると、down_revision に2つの親をタプルで持つ合流用のリビジョンが生成され、upgrade head で2本と合流リビジョンが順に適用されました。

どちらのリビジョンもまだどの環境にも適用していないなら、後から作った側の down_revision をもう一方のIDに書き換えて一直線にすると、履歴を読みやすく保てます。どちらか一方でも共有環境に適用済みなら、既存の履歴は書き換えずmergeで合流させます(公式ドキュメントのbranchesの章)。CIで alembic heads の行数が1であることを確かめておくと、枝分かれしたままのマージを防げます。

本番運用:オフラインモードのSQLレビューとCIでのalembic check

DBに直接接続できない本番環境や、DBAの承認が必要な現場では、--sql を付けると実行する代わりにSQLを出力します。範囲は 開始:終了 で指定します。改名のリビジョンを出力した結果が次です。

$ alembic upgrade 04fcf8a2cc0f:b3a60b421b56 --sql
-- Running upgrade 04fcf8a2cc0f -> b3a60b421b56
ALTER TABLE users RENAME COLUMN name TO full_name;
ALTER TABLE users ADD COLUMN email VARCHAR(255);
UPDATE alembic_version SET version_num='b3a60b421b56' WHERE alembic_version.version_num = '04fcf8a2cc0f';

出力には alembic_version の更新も含まれるので、このSQLを手で流せば版の記録もそろいます。レビューでは DROP を含む行を最初に確認します。

alembic check は、autogenerateと同じ比較を行い、新しいリビジョンが必要な差分があれば失敗します。検証では、差分がないとき終了コード0、モデルだけを変えてリビジョンを作っていないとき(New upgrade operations detected)と、DBが最新まで適用されていないとき(Target database is not up to date.)はどちらも終了コード255でした。CIで空のDBに alembic upgrade head を流した後に alembic check を実行すれば、「モデルは変えたがリビジョンを作り忘れた」プルリクエストを止められます。ただし、checkが見るのはautogenerateが検出できる範囲だけです。改名は追加・削除の差分として検出されますが、改名という意図までは判断できません。サーバー側デフォルト値の比較は既定で無効です。

FastAPIでのディレクトリ構成やテストDBとの組み合わせはFastAPIのベストプラクティスで扱っています。Ruby側で宣言的にスキーマを管理する別の方式は、Ridgepoleの使い方で比較できます。

よくある質問

Alembicの読み方は?

「アレンビック」と読みます。英単語のalembicは蒸留器(ランビキ)の意味です。3DCGで使うAlembic形式(拡張子 .abc)も同じ読みですが、別のプロジェクトです。

AlembicとSQLAlchemyの違いは?

SQLAlchemyはテーブル定義とクエリ発行を担うORM・SQLツールキットで、既存テーブルの変更は扱いません。Alembicはその変更履歴をリビジョンとして管理し、適用と巻き戻しを行います。Alembicを使うにはSQLAlchemyが必要で、1.20.0からはSQLAlchemy 2.0以上が必須です。

alembic downgradeで全部戻すには?

alembic downgrade base を実行すると、すべてのリビジョンの downgrade() が逆順に流れます。戻るのはスキーマだけで、削除された列やテーブルのデータは戻りません。1つだけ戻すなら alembic downgrade -1 です。

autogenerateで空のマイグレーションが生成されるのはなぜ?

モデルが正しく読み込まれていて、DBがすでにモデルと一致していれば、空のファイルが生成されます。これは異常ではありません。注意したいのは逆のケースで、モデルのモジュールをimportし忘れてMetaDataが空になると、空のファイルではなく既存テーブルを消す drop_table が生成されます。target_metadata が None のままなら、ファイルは作られずエラーで止まります。

alembic upgrade headで「Multiple head revisions」と出たら?

履歴が枝分かれしています。alembic heads で末端を確認し、alembic merge heads で合流用のリビジョンを作ってから upgrade head を実行します。どちらも未適用なら、片方の down_revision を書き換えて一直線にする方法もあります。

関連記事

お気に入りに入れた記事の一覧

この記事は以下の記事からリンクされています

資料請求

今日のトレンド記事 直近 24 時間で、いつもより多く読まれている記事

  1. 2026.10.27 テックブログ Brotli 脆弱性:CVE-2026-111497の影響確認とNode.js・Python・.NETの更新手順
  2. 2026.10.28 テックブログ リンツ不正アクセス:314万件の対象確認とパスワード強制リセット後の対処
  3. 2026.10.27 テックブログ Joshin(ジョーシン)不正アクセス:最大600万件の対象とサブスク初期パスワードの対処
  4. 2026.10.23 テックブログ Renta!はいつ再開?不正アクセスでメールアドレス等が流出したおそれ・再開時のパスワード再設定と運営側の対応
  5. 2026.10.28 テックブログ 楽天トラベル不正アクセス:最大約260万件の対象確認とカード下4桁の詐欺対策

RELATED POSTS 関連記事

目次