Open13

alembicとSQLAlchemyでマイグレーションを管理する

shimakaze_softshimakaze_soft
$ alembic init migrations
$ tree
.
├── alembic.ini
└── migrations
    ├── README
    ├── env.py
    ├── script.py.mako
    └── versions

env.py

alembic のツールが起動する度に読み込まれる Python モジュール。

SQLAlchemyのEngineを設定や生成を行って、migrationが実行できるようにカスタマイズする。

README.md

どのような環境でmigration環境を作成したか記述されている。

script.py.mako

新しいmigrationのスクリプトを生成するために使用される Mako テンプレートファイル

ここにあるものは何でもversions内の新しいファイルを生成するために使用される。

versions

migrationスクリプトが保存されるディレクトリ

alembic.ini

alembicのスクリプトが実行される度に読まれる構成ファイル。実行時の設定を記述する。

  • env.py の場所
  • log の出力
  • migration ファイルの命名規則

など

alembic.iniの修正

alembic.init にある、sqlalchemy.url にSQLAlchemy が接続するためのURLを、実行する環境に応じて修正する。

alembic.ini
sqlalchemy.url = sqlite:///test.db

SQLAlchemy.urlを環境変数などから取得する必要がある場合、マイグレーションで複数のデータベースのURLを使用する場合は、env.py ファイルで設定するようにする。

shimakaze_softshimakaze_soft

データベースの設定を行うsettings.pyに以下を記述する。

settings.py
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base

path = 'sqlite:///db.sqlite3'
# path = 'mysql+pymysql://root:@127.0.0.1:3306/alembic_sample'
 
# Engine の作成
Engine = create_engine(
  path,
  encoding="utf-8",
  echo=False
)
Base = declarative_base()

models.pyにモデルの内容を記述する。

models.py
from sqlalchemy import Column, Integer, String

from settings import Base

class User(Base):
    __tablename__ = 'users'

    id = Column(Integer, primary_key=True)
    name = Column(String)
    fullname = Column(String)
    nickname = Column(String)

    def __repr__(self):
        return "<User('name={}', fullname={}, nickname={})>".format(
            self.name,
            self.fullname,
            self.nickname
        )

alembic の環境設定ファイル(env.py) のtarget_metadata を修正します。
アプリケーションのモデルクラスの Base.metada を設定します。

env.py
from settings import Base

target_metadata = Base.metadata

もし複数のモデルクラスを参照する必要があれば、次のように設定します。

env.py
from models import User
from models import User2
target_metadata = [User.metadata, User2.metadata]

env.pyに更に以下を記述する

env.py
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.

  """
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.

    """
    connectable = engine_from_config(
        config.get_section(config.config_ini_section),
        prefix="sqlalchemy.",
        poolclass=pool.NullPool,
    )

    # 追記
    url = config.get_main_option("sqlalchemy.url")

    with connectable.connect() as connection:
        context.configure(
            # 追記
            url=url,
            connection=connection,
            target_metadata=target_metadata
        )

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


if context.is_offline_mode():
    run_migrations_offline()
else:
    run_migrations_online()
shimakaze_softshimakaze_soft

alembic revision ––autogenerateコマンドを実行

$ alembic revision --autogenerate -m "create tables"

versions/配下に下記のmigrationスクリプトファイルが出来上がる。

"""create tables

Revision ID: 11e6228374a5
Revises: 
Create Date: 2022-03-18 10:37:43.272548

"""
from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision = '11e6228374a5'
down_revision = None
branch_labels = None
depends_on = None


def upgrade():
    # ### commands auto generated by Alembic - please adjust! ###
    op.create_table('users',
    sa.Column('id', sa.Integer(), nullable=False),
    sa.Column('name', sa.String(), nullable=True),
    sa.Column('fullname', sa.String(), nullable=True),
    sa.Column('nickname', sa.String(), nullable=True),
    sa.PrimaryKeyConstraint('id')
    )
    # ### end Alembic commands ###


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

shimakaze_softshimakaze_soft

DBのテーブルにSQLAlchemyのモデルを反映

alembic upgrade headを実行することで、テーブルにモデルを反映させることができる。

$ alembic upgrade head
INFO  [alembic.runtime.migration] Context impl SQLiteImpl.
INFO  [alembic.runtime.migration] Will assume non-transactional DDL.
INFO  [alembic.runtime.migration] Running upgrade  -> 11e6228374a5, create tables

テーブルにはalembic_versionusersの2つが生成されているはずです。

alembic_versionの中身を見ると、マイグレーション情報が入っています。

headは最新のversionまでmigrationを実行する。1 つだけ version を上げたい場合は、headではなく+1を使用する。

version を下げたいときは downgrade コマンドを使用する。

shimakaze_softshimakaze_soft

現在の状態を調べる。

$ alembic current
INFO  [alembic.runtime.migration] Context impl SQLiteImpl.
INFO  [alembic.runtime.migration] Will assume non-transactional DDL.
11e6228374a5 (head)

履歴を見る(history)

$ alembic history

更新の巻き戻し(downgrade)

##
# None -> 8d4b3cb61f6 -> 3f1cb7f09b7a という順序で更新が行われている。
# 現在の状態は3f1cb7f09b7a
##

# 一つ前の状態へ戻す
$ alembic downgrade 8d4b3cb61f6
INFO  [alembic.context] Context impl SQLiteImpl.
INFO  [alembic.context] Will assume transactional DDL.

# 現在の状態。8d4b3cb61f6
$ alembic current
INFO  [alembic.context] Context impl SQLiteImpl.
INFO  [alembic.context] Will assume transactional DDL.
Current revision for sqlite:////home/podhmo/.virtualenvs/pyramid/alembic_sample/data.db: None -> 8d4b3cb61f6, create

# baseを指定すると最初に戻す
$ alembic downgrade base
INFO  [alembic.context] Context impl SQLiteImpl.
INFO  [alembic.context] Will assume transactional DDL.
INFO  [alembic.context] Running downgrade 8d4b3cb61f6 -> None

# 現在の状態。None(最初)
$ alembic current
INFO  [alembic.context] Context impl SQLiteImpl.
INFO  [alembic.context] Will assume transactional DDL.
Current revision for sqlite:////home/podhmo/.virtualenvs/pyramid/alembic_sample/data.db: None

# 元の状態に戻す 3f1cb7f09b7a
$ alembic upgrade 3f1cb7f09b7a
INFO  [alembic.context] Context impl SQLiteImpl.
INFO  [alembic.context] Will assume transactional DDL.
INFO  [alembic.context] Running upgrade None -> 8d4b3cb61f6
INFO  [alembic.context] Running upgrade 8d4b3cb61f6 -> 3f1cb7f09b7a
shimakaze_softshimakaze_soft

SQLAlchemyと非同期処理

通常のSQLAlchemyの使用方法では、非同期処理に対応しておらず、FastAPIのasync/awaitによるイベントループを活用した高速なDB処理を行うことができません。ORMは遅延読み込みを多用するため非同期処理に対応するのが難しく、元来SQLAlchemyの低レイヤ実装であるSQLAlchemy Coreによる、ORMよりもprimitiveな書き方(クエリを直接書き下すのに近い記法)でしか非同期処理に対応していませんでした。

SQLAlchemyのバージョン1.4からは、2.0 Styleという新しい書き方によりORMとしてクラスを定義した場合でも、非同期処理がサポートされています。本書ではこちらの書き方に基づき、高速なDBアクセスを可能にします。

shimakaze_softshimakaze_soft

alembicのデメリット

  • テーブル名の変更(drop/addになりデータが初期化されます)
  • カラム名の変更(drop/addになりデータが初期化されます)
  • 名前の付いていないユニーク制約 (変更を検知するにはユニーク制約に名前をつけることは必須(e.g. - UniqueConstraint('col1', 'col2', name="my_name"))
  • EnumなどのSQLAlchemy特有のカラムの変更
  • Columnの順番の指定