utamaro’s blog

誰かの役に立つ情報を発信するブログ

alembicで環境別にマイグレーションファイルを分割する

alembicというマイグレーション管理ライブラリを利用したマイグレーションファイルの分割方法について。

実現したいのは、開発環境のrevisionと本番環境用のrevisionを分けたいということです。

分けることで、リリースタイミングですべてのテストが完了したrevisionのみ作成されて、マイグレーションファイルをきれいに管理できるようにするためです。

また、本番リリースのタイミングで開発環境上のマイグレーションを破棄して、本番用のバージョンに置き換えるということもできます。

最終的なディレクトリ構成は以下のようになります。

.
├── alembic.dev.ini
├── alembic.prod.ini
├── app
│   ├── __init__.py
│   └── models
│       ├── __init__.py
│       └── users.py
├── app.py
├── db
│   ├── __init__.py
│   ├── migrate_dev
│   │   ├── README
│   │   ├── env.py
│   │   ├── script.py.mako
│   │   └── versions
│   │       ├── 2020_02_23_123853_.py
│   │       ├── 2020_02_23_125416_.py
│   │       └── __init__.py
│   └── migrate_prod
│       ├── README
│       ├── env.py
│       ├── script.py.mako
│       └── versions
│           ├── 2020_02_23_125959_.py
│           └── __init__.py
└── static

準備

依存ライブラリ

SQLAlchemy==1.3.13
alembic==1.4.0
mysqlclient==1.4.6

pythonのバージョン

Python 3.8.0

環境毎に初期化を行う

※ 一つ一つメモを取りつつ行ったわけではないため、コマンド実行時の結果に差がでる可能性があります。ご参考程度によろしくおねがいします。

initコマンド使用して初期化します。

alembic init db/migrate_dev

このとき、カレントディレクトリ内にalembic.iniというファイルが作成されます。 このファイルをalembic.dev.iniにリネームします。

mv alembic.ini alembic.dev.ini
alembic init db/migrate_prod

こちらも同様にalembic.prod.iniにリネームしてください。

mv alembic.ini alembic.prod.ini

環境別のenvを修正する

devprodenv.pyは同じことを書いてください。

import importlib
import os
import sys
from logging.config import fileConfig

from sqlalchemy import engine_from_config, MetaData
from sqlalchemy import pool

from alembic import context

config = context.config

fileConfig(config.config_file_name)

BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
MODELS_ROOT = BASE_DIR + '/../app/models'
sys.path.append(MODELS_ROOT)

target_models = [
    "users",
]


class BaseEnv:
    @staticmethod
    def make_target_metadata():
        lst = list(map(lambda x: importlib.import_module(x).Base.metadata, target_models))
        m = MetaData()
        for metadata in lst:
            for t in metadata.tables.values():
                t.tometadata(m)
        return m


def run_migrations_online():
    """Run migrations in 'online' mode.

    In this scenario we need to create an Engine
    and associate a connection with the context.

    """

    alembic_config = config.get_section(config.config_ini_section)
    connectable = engine_from_config(
        alembic_config,
        prefix="sqlalchemy.",
        poolclass=pool.NullPool,
    )
    target_metadata = BaseEnv.make_target_metadata()
    with connectable.connect() as connection:
        context.configure(
            connection=connection,
            target_metadata=target_metadata,
            compare_type=True,
        )

        with context.begin_transaction():
            context.run_migrations()


run_migrations_online()

今回はrevision --autogenerateで自動的にモデルを読み込むためにBaseEnv.make_target_metadata()を作成しました。

残念ながらenv.pyが実行されるタイミングがalembic revisionが実行されたタイミングなのでapp以下のmodelsを読み込むのが難しいです。

そのために、以下のようにモデルまでのパスを登録しています。

BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
MODELS_ROOT = BASE_DIR + '/../app/models'
sys.path.append(MODELS_ROOT)

これを行うことで、以下のようにモデルを定義できます。

target_models = [
    "users",
]

最後にメタデータを取得するための関数を通して、target_metadataを作成しています。

usersテーブル用のモデルを作成する

よくある書き方なので詳細については省略します。

from sqlalchemy import DATETIME, Column, Integer, String, BigInteger
from sqlalchemy.ext.declarative import declarative_base

Base = declarative_base()


class User(Base):
    __tablename__ = "users"

    id = Column(BigInteger, primary_key=True, nullable=False)
    name = Column(String(255), nullable=False)
    email = Column(String(255), nullable=False)
    created_at = Column(DATETIME, nullable=False)
    updated_at = Column(DATETIME, nullable=False)

.iniファイルを修正する

initで作成されたファイルのうち、[alembic]を以下のように修正します。

[alembic]
script_location = db/migrate_dev
file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d%%(second).2d_%%(slug)s
sqlalchemy.url = mysql://root:admin@localhost:3306/sample_a

※ sqlalchemyとmysqlclientを利用しているため、sqlalchemy.urlは適宜読み替えてください。

同様にalembic.prod.iniも修正します。

[alembic]
script_location = db/migrate_prod
file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d%%(second).2d_%%(slug)s
sqlalchemy.url = mysql://root:admin@localhost:3306/sample_b

今回はわかりやすく別スキーマを用意して、マイグレーション対象のDBを分けました。

これで、alembic.dev.iniの場合はsample_aDBに対してマイグレーションが実行され、alembic.prod.iniの場合はsample_bDBに対して実行されるようになります。

revisionを作成する

revisionを作成する際に以下のことが必要です。

  1. .iniを指定する。
  2. modelsから自動的に読み込む。

これを以下のコマンドで実現します。

alembic -c ./alembic.prod.ini revision --autogenerate

実行後にdb/migrate_prod/versions2020_02_23_123612_.pyというファイルが作成されます。

その中が以下のようになっていれば成功です。

def upgrade():
    # ### commands auto generated by Alembic - please adjust! ###
    op.create_table('users',
    sa.Column('id', sa.BigInteger(), nullable=False),
    sa.Column('name', sa.String(length=255), nullable=False),
    sa.Column('email', sa.String(length=255), nullable=False),
    sa.Column('created_at', sa.DATETIME(), nullable=False),
    sa.Column('updated_at', sa.DATETIME(), nullable=False),
    sa.PrimaryKeyConstraint('id')
    )
    # ### end Alembic commands ###


def downgrade():
    # ### commands auto generated by Alembic - please adjust! ###
    op.drop_table('users')
    # ### end Alembic commands ###

同様にdevも実行してみてください。

alembic -c ./alembic.dev.ini revision --autogenerate

DBをマイグレーションする

作成された2020_02_23_123612_.pyをDBに反映させます。

alembic -c alembic.dev.ini upgrade head

これを実行すると、sample_aスキーマusersテーブルが生成され、alembic_versionテーブルにRevision IDが追加されます。

devで作業して、成果物をprodに反映する

モデルを修正して、スキーママイグレーションします。

今回はUserモデルにdeletedを追加します。

deleted = Column(Boolean, default=False)

修正分のリビジョンを作成します。

alembic -c ./alembic.dev.ini revision --autogenerate

2020_02_23_125416_.pyが作成されて、以下のスクリプトが生成されました。

def upgrade():
    # ### commands auto generated by Alembic - please adjust! ###
    op.add_column('users', sa.Column('deleted', sa.Boolean(), nullable=True))
    # ### end Alembic commands ###


def downgrade():
    # ### commands auto generated by Alembic - please adjust! ###
    op.drop_column('users', 'deleted')
    # ### end Alembic commands ###

マイグレーションします。

alembic -c alembic.dev.ini upgrade head

これでdeletedカラムが追加されます。

この段階でdb/migrate_dev/versionsの中身は以下のようになっています。

db/migrate_dev
├── README
├── env.py
├── script.py.mako
└── versions
    ├── 2020_02_23_123853_.py
    ├── 2020_02_23_125416_.py
    └── __init__.py

この段階でdb/migrate_prod/versionsの中身は以下のようになっています。

(migrate_prodversionsに複数のファイルが含まれておらず、きれいなままです。)

db/migrate_prod
├── README
├── env.py
├── script.py.mako
└── versions
    └── __init__.py

usersテーブルの成果物を本番環境に反映します。

alembic -c ./alembic.prod.ini revision --autogenerate

以下のようにスクリプトが生成されます。

def upgrade():
    # ### commands auto generated by Alembic - please adjust! ###
    op.create_table('users',
    sa.Column('id', sa.BigInteger(), nullable=False),
    sa.Column('name', sa.String(length=255), nullable=False),
    sa.Column('email', sa.String(length=255), nullable=False),
    sa.Column('deleted', sa.Boolean(), nullable=True),
    sa.Column('created_at', sa.DATETIME(), nullable=False),
    sa.Column('updated_at', sa.DATETIME(), nullable=False),
    sa.PrimaryKeyConstraint('id')
    )
    # ### end Alembic commands ###


def downgrade():
    # ### commands auto generated by Alembic - please adjust! ###
    op.drop_table('users')
    # ### end Alembic commands ###

あとはマイグレーションするだけです。

alembic -c alembic.prod.ini upgrade head

これでsample_bスキーマusersテーブルが生成されました。

参考

https://alembic.sqlalchemy.org/en/latest/tutorial.html

http://okerra.hatenablog.com/entry/2020/02/05/184237