マイグレーションを使ってDBに初期データを入れる方法

## 背景
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に追加**

```txt
alembic==1.13.0
```

### **1.2 alembicの初期化**
```bash
# コンテナ内で実行
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の設定**
インポート文にアプリで定義しているモデルのベースクラスをインポート
これはモデルのベースクラスを定義していたのでそれをインポートしました。
```python
#app/alembic/env.py
# ---他のインポート文---
from app.db.base import Base  # モデルのベースクラス

# ---他の設定の記述---
```

### **1.6 最初のマイグレーションファイルの生成**

```bash
# マイグレーションファイルを自動生成
docker compose exec {アプリ名} alembic revision --autogenerate -m "create initial tables"
```

`alembic revision` コマンドでマイグレーションファイルを作成します。 `--autogenerate` オプションをつけることでSQLAlchemyのモデル定義とデータベースの現状を比較し、その差分を検出して新しいマイグレーションファイルを自動生成します。
この段階ではマイグレーションファイルを作成するだけでまだ実行はされていません。

### **1.7 マイグレーションの実行**

```bash
# マイグレーションを実行
docker compose exec {アプリ名} alembic upgrade head
```

これでマイグレーションが実行されました。

### **1.8 マイグレーションの確認**

```bash
# 現在のマイグレーション状態を確認
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 マイグレーション履歴の確認**

```bash
# マイグレーション履歴を確認
docker compose exec app alembic history
```

## 1.10 追加データ用のマイグレーションファイル作成

今回私は`varsions`ディレクトリ内のマイグレーションファイルを新しく作成し、初期データを追加するマイグレーションを自分で作成しました。

```bash
docker exec コンテナ名 alembic revision -m "seed initial ai models"
```
`alembic revision`コマンドに特にオプションを指定しないと空のマイグレーションファイルが作成されます。（正確にはupgradeとdowngrade関数内の記述が何もない状態）

そのファイルを以下のように書きました。

```python
"""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`を作成し、その中を以下の様にしました。

```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`で読み込む様にします。

```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を使うときの実行したコマンドをいくつかピックアップします。

## **エラーが発生した場合のリセット手順**

### **マイグレーションのロールバック**

```bash
# マイグレーションを最初の状態に戻す
docker compose exec app alembic downgrade base
```

### **現在のマイグレーションの状態を確認**

```bash
docker compose exec app alembic current
```

### **マイグレーションファイルの削除**

```bash
# マイグレーションファイルを削除
rm app/alembic/versions/*
```

### **データベースのリセット**

```bash
# コンテナとボリュームを停止・削除
docker compose down -v

# コンテナを再起動
docker compose build --no-cache
docker compose up 
```

### **マイグレーションのやり直し**

```bash
# マイグレーションファイルを再生成
docker compose exec app alembic revision --autogenerate -m "create initial tables"

# マイグレーションを実行
docker compose exec app alembic upgrade head

```


### **特定のバージョンへのロールバック**

```bash
# 特定のリビジョンIDにロールバック
docker compose exec app alembic downgrade リビジョンID
```

## まとめ

以上今回アプリにマイグレーションを導入し、それを利用して初期値の設定をすることができました。
これがベストプラクティスでは無いかと思いますが、マイグレーションについての学びにはなったと感じております。
少しでも参考になれば幸いです。