背景
FastAPIでアプリを作成している時に、マイグレーションについて改めて考える機会がありましたのでまとめました。
以前Djangoでアプリを作成していた時もマイグレーションは行なっていましたが、Djangoの場合プロジェクトを立ち上げた時にマイグレーション機能もセットになっていて、DBの作成時や変更があった時はやらないといけないものとしてやっていました。
今回はFastAPIを使用しており、別にマイグレーションしなくてもアプリは動作するしわざわざ導入しなくてもいいのでは?と思い導入せずにいましたが、やっぱり入れた方が良いなと途中で思い直しPythonのマイグレーションをサポートしているalembicを導入しました。
マイグレーションとは?
データベースのテーブルやカラムの変更などデータベース構造に関するあらゆる変更をバージョン管理する手法です。
マイグレーションのメリット
- データベースの構造で問題が出た時にロールバック(以前の状態に戻す)ことができる
- データベースの変更履歴がわかる
- 環境間の一貫性確保(本番、開発環境での違いやMySQLのバージョン違いとかでも問題ない)
マイグレーションを使用しないとどうなる?
- 変更履歴が無いのでロールバックできない
- チーム開発の場合、メンバー各々が手動でテーブル変更をする必要がある
マイグレーションを導入した理由
アプリを作成している途中まではマイグレーションをわざわざ導入しなくてもいいかなと思っていましたが、アプリではAIを使ったチャット機能を実装しており、AIモデルを選択できるようにしていました。
チャットをする際AIモデルテーブルから使用するモデルを呼び出す必要がありましたが、テーブルにはデータが入っていないためエラーになっていました。
調べるうちにマイグレーションを活用して初期データを投入できそうだと思ったのと、マイグレーションについて深ぼるいい機会だと思い導入しました。
もちろん今後継続的に開発を行うことを前提にした場合、テーブルの変更履歴があった方が良いと思ったのもあります。
alembicとは?
Alembicは、PythonのSQLAlchemyライブラリと連携して動作するデータベースマイグレーションツールです。
alembicの使い方
今回dockerを使用していたため、dockerコンテナ内に入ってコマンドを実行しています。
※ dockerを使用しない場合は docker exec
コマンドを除いて実行してください
※ {}
で囲んだ部分は各環境で適切な値に置き換えてください
※ app
は今回使用したバックエンドのディレクトリ名です。
1.1 requirement.txtに追加
alembic==1.13.0
1.2 alembicの初期化
# コンテナ内で実行
docker compose exec {コンテナ名} alembic init app/alembic
1.3 設定の確認
初期化を行うとalmbicディレクトリが作成され、その中にファイルがいくつか自動生成されます。
-
versionsディレクトリ
- マイグレーションスクリプトが保存されるディレクトリ
-
env.py
- alembic のツールが起動する度に読み込まれる。SQLAlchemy の Engine を設定や生成を行って、 migration が実行できるようにカスタマイズする
-
README
- どのような環境で migration 環境を作成したか記述されている
-
script.py.mako
- マイグレーションファイルのテンプレートとして機能
- alembic revision コマンドを実行した時に、このテンプレートを基に新しいマイグレーションファイルが生成される
-
alembic.ini
- alembicの設定ファイル。データベースの接続や動作設定など
上記ファイルのalembic.ini
のデータベース接続URLを確認し、適切なURLを設定します。
sqlalchemy.url = mysql+pymysql://root:{DBのパスワード}@{DBホスト}/{DB名}
1.5 env.pyの設定
インポート文にアプリで定義しているモデルのベースクラスをインポート
これはモデルのベースクラスを定義していたのでそれをインポートしました。
#app/alembic/env.py
# ---他のインポート文---
from app.db.base import Base # モデルのベースクラス
# ---他の設定の記述---
1.6 最初のマイグレーションファイルの生成
# マイグレーションファイルを自動生成
docker compose exec {アプリ名} alembic revision --autogenerate -m "create initial tables"
alembic revision
コマンドでマイグレーションファイルを作成します。 --autogenerate
オプションをつけることでSQLAlchemyのモデル定義とデータベースの現状を比較し、その差分を検出して新しいマイグレーションファイルを自動生成します。
この段階ではマイグレーションファイルを作成するだけでまだ実行はされていません。
1.7 マイグレーションの実行
# マイグレーションを実行
docker compose exec {アプリ名} alembic upgrade head
これでマイグレーションが実行されました。
1.8 マイグレーションの確認
# 現在のマイグレーション状態を確認
docker compose exec {アプリ名} alembic current
# データベースに接続しテーブルを確認
docker compose exec {DB名} mysql -u root -p
# MySQLのパスワードを求められるので入力。以下MySQL内のコマンド
# データベースを確認
show databases;
# データベースに移動
use {db名};
# テーブル確認
show tables;
# データを確認
select * from {テーブル名};
# MySQLから抜ける
exit;
1.9 マイグレーション履歴の確認
# マイグレーション履歴を確認
docker compose exec app alembic history
1.10 追加データ用のマイグレーションファイル作成
今回私はvarsions
ディレクトリ内のマイグレーションファイルを新しく作成し、初期データを追加するマイグレーションを自分で作成しました。
docker exec コンテナ名 alembic revision -m "seed initial ai models"
alembic revision
コマンドに特にオプションを指定しないと空のマイグレーションファイルが作成されます。(正確にはupgradeとdowngrade関数内の記述が何もない状態)
そのファイルを以下のように書きました。
"""seed initial ai models
Revision ID: 8f9d49259084
Revises: 73c6db1e2ae3
Create Date: 2024-12-02 05:21:09.721243
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = '8f9d49259084'
down_revision: Union[str, None] = '73c6db1e2ae3'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# 初期データの投入
op.execute("""
INSERT INTO ai_models (ai_model_id, ai_model_name, description)
VALUES
(1, 'GPT-3.5-turbo', 'OpenAI GPT-3.5モデル'),
(2, 'GPT-4o', 'OpenAI GPT-4モデル'),
(3, 'Claude-3-5-sonnet', 'Anthropic Claudeモデル')
ON DUPLICATE KEY UPDATE
ai_model_name = VALUES(ai_model_name),
description = VALUES(description)
""")
def downgrade() -> None:
op.execute("""
DELETE FROM ai_models
WHERE ai_model_name IN (
'GPT-3.5-turbo',
'GPT-4o',
'Claude-3-5-sonnet'
)
""")
docker compose ymlの編集とコマンド用ファイルの作成
今回docker compose up
をして自動でマイグレーションして欲しかったのでコマンド実行用のentrypoint.sh
を作成しました。
プロジェクト直下にdocker/app/entrypoint.sh
を作成し、その中を以下の様にしました。
echo "Waiting for database to be ready..."
while ! nc -z db 3306; do
sleep 1
done
echo "Database is ready!"
# マイグレーションを実行
echo "Running migrations..."
cd /app
alembic upgrade head
# FastAPIアプリケーションを起動
echo "Starting FastAPI application..."
uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload
-
nc -z db 3306
: MySQLデータベース(ホスト名:db、ポート:3306)への接続を確認- 接続できない場合は1秒待って再試行
- これにより、データベースが完全に起動するまでアプリケーションの起動を待機
-
alembic upgrade head
: マイグレーションの実行- 最新のマイグレーションまで全て実行します
-
vicorn app.main:app --host 0.0.0.0 --port 8000 --reload
: FastAPIアプリケーションの起動
この様にコマンドをまとめたファイルを最後にdocker-compose.yml
で読み込む様にします。
services:
app:
build: .
container_name: {コンテナ名}
ports:
- "8000:8000"
depends_on:
- db
env_file:
- .env
volumes:
- .:/app
entrypoint: ["sh", "/app/docker/app/entrypoint.sh"]
これでdocker compose up
した時に自動でマイグレーションをしてくれる設定が完了しました。
補足でalembicを使うときの実行したコマンドをいくつかピックアップします。
エラーが発生した場合のリセット手順
マイグレーションのロールバック
# マイグレーションを最初の状態に戻す
docker compose exec app alembic downgrade base
現在のマイグレーションの状態を確認
docker compose exec app alembic current
マイグレーションファイルの削除
# マイグレーションファイルを削除
rm app/alembic/versions/*
データベースのリセット
# コンテナとボリュームを停止・削除
docker compose down -v
# コンテナを再起動
docker compose build --no-cache
docker compose up
マイグレーションのやり直し
# マイグレーションファイルを再生成
docker compose exec app alembic revision --autogenerate -m "create initial tables"
# マイグレーションを実行
docker compose exec app alembic upgrade head
特定のバージョンへのロールバック
# 特定のリビジョンIDにロールバック
docker compose exec app alembic downgrade リビジョンID
まとめ
以上今回アプリにマイグレーションを導入し、それを利用して初期値の設定をすることができました。
これがベストプラクティスでは無いかと思いますが、マイグレーションについての学びにはなったと感じております。
少しでも参考になれば幸いです。