参考

https://www.sria.co.jp/blog/2021/06/5557/

マイグレーションツールについて

FastAPIとかのフレームワークを使う際にでDB関係をいじるときは、自分でmigrationツールとORMを選ぶ必要があるんですが、migrationツールの選定は注意が必要っぽいです。

ORMでsqlalchemyを使うならsqlalchemy-migrateではなくalembicを使った方がいいらしいです。

SQLAlchemy-migrateは第三者が作ってるものらしくて、alembicはSQLAlchemyの作者(Mike Bayer)が考える最強のMigrationツールとして Alembicを作ったらしいです

名前的にはSQLAlchemy-migrateがオフィシャルっぽい感じがするのでややこしいですねw

どうやら経緯としては

  1. SQLAlchemy-migrate が第三者によって作られる。
  2. メンテする人がいなくなり、 SQLAlchemy の作者(Mike Bayer)がメンテナーみたいな風潮になる
  3. その Mike Bayer は、自分が考える最強の Migration ツールとして Alembic を作る

らしいですw

使い方

事前にpostgresとかのセッティングはしておきましょう。
今回自分はpostgresを使ってます

alembic init test

このあと、databse.pyとかのファイルを適当に作ってDB接続エンジンを作ります

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

engine = create_engine('postgresql://username:password@localhost:5432/onsen_mania')
Base = declarative_base()

そのあとmodels.pyを作成してモデルの設定を記述していきます

from sqlalchemy import Column, DateTime, Integer, String, Text, Float, Boolean
from sqlalchemy.ext.declarative import declared_attr
from datetime import datetime
from database import Base


class TimestampMixin(object):
    @declared_attr
    def created_at(cls):
        return Column(DateTime, default=datetime.now(), nullable=False)

    @declared_attr
    def updated_at(cls):
        return Column(
            DateTime, default=datetime.now(), onupdate=datetime.now(), nullable=False
        )

class Onsen(Base, TimestampMixin):
    __tablename__ = 'onsen'

    id = Column(Integer, primary_key=True)
    name       = Column(String)
    address    = Column(String)
    tel        = Column(String)
    open_time  = Column(String)
    off_day    = Column(String)
    parking    = Column(String)
    price      = Column(String)
    caution    = Column(Text)
    sauna      = Column(String)
    image_path = Column(String)
    lat        = Column(Float)
    lon        = Column(Float)
    pref       = Column(String)

    def __repr__(self):
        return "<Onsen('name={}', address={}, tel={}, open_time={}, off_day={}, parking={}, price={}, caution={}, sauna={}, image_path={}, lat={}, lon={})>".format(
            self.name,
            self.address,
            self.tel,
            self.open_time,
            self.off_day,
            self.parking,
            self.price,
            self.caution,
            self.sauna,
            self.image_path,
            self.lat,
            self.lon,
            self.pref
        )

ざっくりこんな感じ

そのあと、alembic.iniの設定をする
基本は上で作ったエンジンと書き方は同じ。ドキュメントや上の記事を参照

env.pyでモデルを読み込む

from logging.config import fileConfig

from sqlalchemy import engine_from_config
from sqlalchemy import pool

from alembic import context
from models import Onsen, Users
# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.
config = context.config

# Interpret the config file for Python logging.
# This line sets up loggers basically.
if config.config_file_name is not None:
    fileConfig(config.config_file_name)

# add your model's MetaData object here
# for 'autogenerate' support
from database import Base
target_metadata = Base.metadata
# target_metadata = None

# other values from the config, defined by the needs of env.py,
# can be acquired:
# my_important_option = config.get_main_option("my_important_option")
# ... etc.


def run_migrations_offline() -> None:
    """Run migrations in 'offline' mode.

    This configures the context with just a URL
    and not an Engine, though an Engine is acceptable
    here as well.  By skipping the Engine creation
    we don't even need a DBAPI to be available.

    Calls to context.execute() here emit the given string to the
    script output.

    """
    url = config.get_main_option("sqlalchemy.url")
    context.configure(
        url=url,
        target_metadata=target_metadata,
        literal_binds=True,
        dialect_opts={"paramstyle": "named"},
    )

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


def run_migrations_online() -> None:
    """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(
            connection=connection, url=url, target_metadata=target_metadata
        )

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


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

ここまでできれば

alembic revision --autogenerate -m "create tables"
alembic upgrade head # create
alembic downgrade base # erase all

これで逐次進捗を確認するとテーブルが作成されているのが確認できるはず

テーブルを追加したりするときは、models.pyで変更を加えるとか、新しいクラスをフォーマットに従って定義して上のコマンドを打つとマイグレーションスクリプトが作成されるので超便利

すごいなぁ