先日書いたFastAPI+SQLAlchemyで非同期WebAPIで紹介したrhoboro/async-fastapi-sqlalchemyをより本格的に使えるようにAlembicを導入しました。 変更内容はPR:DBスキーマの管理をAlembicで行うにありますが、行ったことをざっとまとめておきます。
Alembicの導入
インストールはpip install
で入るので特筆することはありません。
(venv) $ pip install alembic
インストールできたら、initコマンドを実行して設定ファイルなどを作成します。
今回はasyncpg
を使いたいので--template async
オプションをつけて実行しました。
最後の引数は各種スクリプトの格納先です。
デフォルトはalembic/ですが、わたしはいつもmigrations/で管理しています。
(venv) $ alembic init --template async migrations
initコマンドを実行するとalembic.ini
と下記のmigrations/
が作成されます。(このコミット)
Alembicでは全体的な設定をalembic.ini
で管理し、細かな挙動はenv.py
でカスタマイズします。
マイグレーションファイルはversions/
配下に格納していき、script.py.mako
がそれらのマイグレーションファイルのテンプレートになっています。
(venv) $ ls alembic.ini
alembic.ini
(venv) $ tree migrations
migrations
├── README
├── env.py
├── script.py.mako
└── versions
1 directory, 3 files
設定ファイルの更新
設定ファイルが作成できたら、今回のプロジェクト内容に沿って更新します。(このコミットとこのコミット)
alembic.ini
ローカルでの動作確認やマイグレーションファイルの生成にはDockerのpostgresイメージで立てたコンテナを使っているので、sqlalchemy.urlの値をそのDockerコンテナ用の接続情報にしています。
ただし、この値はAlembicをオフラインモードで実行したときのみ利用されます。
実際にはローカルのコンテナとアプリケーション本体で使うengineオブジェクトを使ってオンラインモードを利用するため、わたしはオフラインモードはほとんど利用していません。
そのほか、マイグレーションファイルの生成時に自動でblackによるフォーマットが実行されるようにしています。
migrations/script.py.mako
こちらの変更はわたしがいつも行っているものです。
Alambicはマイグレーションファイルを自動生成できますが、「# ### commands auto generated by Alembic - please adjust! ###」というコメントがあるように、その処理内容は最終的には開発者に委ねられています。
チーム開発では複数人がマイグレーションファイルを生成すると思いますが、その際に少しでも統一感を保てるようにマイグレーションの前後で行う処理を書くpre_upgrade(), post_upgrade(), pre_downgrade(), post_downgrade()を追加しています。
これらは、マイグレーション前にデータ整合性を整えたり、マイグレーション後に初期データを投入する際などに利用しています。
migrations/env.py
env.pyには複数の変更がはいっているので、それぞれまとめました。
まず、target_metadata
にはSQLAlchemy ORMのmetadataオブジェクトを渡します。
このmetadataからORMの各モデルの情報、すなわち各テーブルのスキーマ情報が得られるため、マイグレーションファイルの自動生成が可能になります。
include_object()
では、自動生成の対象外としたいモデルクラスがある場合にモデル側で__table_args__ = {"info": {"skip_autogen": True}}
と宣言するだけで対象外にできるようにしています。
これは、VIEWなどの参照専用で使いたいモデルクラスを定義する際に利用しています。
compare_type
はカラムの型変更を自動で検知するためのフラグです。
compare_type=Falseの場合、マイグレーションファイルの自動生成時にカラムの変更が検知されないので注意が必要です。
Naming Convention
これは制約やインデックスにつける名前の設定です。 実際の設定内容はこちらにあります。
これ自体は設定ファイルの更新ではないのですが、デフォルトではSQLAlchemyもAlembicも制約に名前をつけないため、ダウングレードなどがうまく動かないことがあります。 そのため、新規アプリケーションなどでは最初から設定しておくのがオススメです。 詳細はConfiguring a Naming Convention for a MetaData Collectionにあります。
マイグレーションファイルの生成
ここまで設定できたら、alembic revision
コマンドでマイグレーションファイルを生成できます。
初回の実行時のみオフラインモードで空のマイグレーションファイルを生成しておくと、そのリビジョンを指定してダウングレードするだけでDBを初期化できて便利です。
なお、ここでは環境変数APP_CONFIG_FILE
でDB接続情報などの設定値を切り替えています。
マイグレーションファイルの生成はローカルで行い、開発環境や本番環境のDBにマイグレーションを適用する場合には、APP_CONFIG_FILE=devやAPP_CONFIG_FILE=prodを指定しています。
# 以降では、このコマンドで起動したpostgresコンテナを使っています
# (venv) $ docker run -d --name db \
# -e POSTGRES_PASSWORD=password \
# -e PGDATA=/var/lib/postgresql/data/pgdata \
# -v $(pwd)/pgdata:/var/lib/postgresql/data \
# -p 5432:5432 \
# postgres:13.3
# DB接続なしで空のマイグレーションファイルを用意
(venv) $ APP_CONFIG_FILE=local alembic revision -m 'initial_empty'
Generating /Users/rhoboro/go/src/github.com/rhoboro/async-fastapi-sqlalchemy/migrations/versions/a8483365f505_initial_empty.py ... done
Running post write hook "black" ...
reformatted /Users/rhoboro/go/src/github.com/rhoboro/async-fastapi-sqlalchemy/migrations/versions/a8483365f505_initial_empty.py
All done! ✨ 🍰 ✨
1 file reformatted.
done
空のマイグレーションファイルを作成できたら、それをDBに適用します。 マイグレーションファイルがある場合、DBの状態を最新にするまで次のマイグレーションファイルの自動生成はできません。
# 最新状態までのマイグレーション適用
(venv) $ APP_CONFIG_FILE=local alembic upgrade head
INFO [alembic.runtime.migration] Context impl PostgresqlImpl.
INFO [alembic.runtime.migration] Will assume transactional DDL.
INFO [alembic.runtime.migration] Running upgrade -> a8483365f505, initial_empty
続いて、モデルクラスからマイグレーションファイルを生成していきます。
alembic revisionコマンドに--autogenerate
オプションをつけることでオンラインモードとなり、DBの状態とモデルクラスとの差分からマイグレーションファイルを自動生成してくれます。
# マイグレーションの自動生成
(venv) $ APP_CONFIG_FILE=local alembic revision --autogenerate -m 'add_tables'
INFO [alembic.runtime.migration] Context impl PostgresqlImpl.
INFO [alembic.runtime.migration] Will assume transactional DDL.
INFO [alembic.autogenerate.compare] Detected added table 'notebooks'
INFO [alembic.autogenerate.compare] Detected added table 'notes'
Generating /Users/rhoboro/go/src/github.com/rhoboro/async-fastapi-sqlalchemy/migrations/versions/24104b6e1e0c_add_tables.py ... done
Running post write hook "black" ...
reformatted /Users/rhoboro/go/src/github.com/rhoboro/async-fastapi-sqlalchemy/migrations/versions/24104b6e1e0c_add_tables.py
All done! ✨ 🍰 ✨
1 file reformatted.
done
# 最新状態までのマイグレーション適用
(venv) $ APP_CONFIG_FILE=local alembic upgrade head
INFO [alembic.runtime.migration] Context impl PostgresqlImpl.
INFO [alembic.runtime.migration] Will assume transactional DDL.
INFO [alembic.runtime.migration] Running upgrade a8483365f505 -> 24104b6e1e0c, add_tables
ここまでエラーなく進むと、テーブルが作成された状態になっています。 コンテナであればエラーが出ても何度でも作って壊して試せるので便利ですね。
(venv) $ psql -h localhost -U postgres postgres
Password for user postgres:
psql (13.2, server 13.3 (Debian 13.3-1.pgdg100+1))
Type "help" for help.
postgres=# \d
List of relations
Schema | Name | Type | Owner
--------+------------------+----------+----------
public | alembic_version | table | postgres
public | notebooks | table | postgres
public | notebooks_id_seq | sequence | postgres
public | notes | table | postgres
public | notes_id_seq | sequence | postgres
(5 rows)