dockerで再起動した際のマイグレーションエラーの解決方法


# Alembicマイグレーション失敗時の対処法：「relation already exists」エラーの解決

## はじめに

Docker環境でFastAPI + Alembicを使用している際に、コンテナ再起動後にマイグレーションが失敗する問題と、その完全な解決方法をまとめました。

特に、Firebase認証への移行に伴うデータベーススキーマ変更時に発生した実例を基に、実践的な解決方法を解説します。

## 発生したエラー

### エラー内容

```bash
sqlalchemy.exc.ProgrammingError: (psycopg2.errors.DuplicateTable) relation "users" already exists

[SQL: 
CREATE TABLE users (
      id UUID NOT NULL, 
      email VARCHAR(255) NOT NULL, 
      password VARCHAR(255) NOT NULL,  ← 古いスキーマ
      role INTEGER NOT NULL, 
      ...
)
```

### エラーの根本原因

1. **マイグレーション履歴の不整合**: Alembicの管理テーブル（alembic_version）とデータベースの実際の状態が合っていない
2. **古いマイグレーションファイル**: 古いスキーマ定義（passwordカラムあり）が残っている  
3. **モデル更新後の競合**: 現在のモデル（firebase_uidあり）と既存マイグレーションの不一致

## 環境構成

### Docker構成

```yaml
# docker-compose.yml
services:
  web:
    build: .
    command: [
      "/bin/sh",
      "-c", 
      "alembic upgrade heads && uvicorn app.main:app --host 0.0.0.0 --port 80 --reload"
    ]
    volumes:
      - .:/backend  # ホスト側ファイルがマウント
    depends_on:
      db:
        condition: service_healthy
  
  db:
    image: postgres:16
    environment:
      POSTGRES_DB: baseball_note
      POSTGRES_USER: docker
      POSTGRES_PASSWORD: docker
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U docker -d baseball_note"]
```

### モデル変更内容

```python
# 変更前のモデル（base.py）
class Users(Base, ModelBaseMixin):
    __tablename__ = "users"
    
    id: Mapped[UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
    email: Mapped[str] = mapped_column(String(255), nullable=False, unique=True)
    password: Mapped[str] = mapped_column(String(255), nullable=False)  # 削除対象
    role: Mapped[int] = mapped_column(Integer, nullable=False, default=UserRole.PLAYER)
```

```python
# 変更後のモデル（base.py）
class Users(Base, ModelBaseMixin):
    __tablename__ = "users"
    
    id: Mapped[UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
    firebase_uid: Mapped[str] = mapped_column(String(128), nullable=False, unique=True)  # 追加
    email: Mapped[str] = mapped_column(String(255), nullable=False, unique=True)
    role: Mapped[int] = mapped_column(Integer, nullable=False, default=UserRole.PLAYER)
```

## 解決方法

### Method 1: 完全リセット（開発環境推奨）

最もクリーンで確実な方法です。**開発環境でのみ使用してください。**

```bash
# 1. 完全停止とボリューム削除
docker-compose down -v

# 2. 古いマイグレーションファイルを全削除
rm ./alembic/versions/*.py

# 3. コンテナを再起動
docker-compose up -d --build

# 4. 新しいマイグレーションを作成
docker exec -it コンテナ名 bash
alembic revision --autogenerate -m "create_initial_tables_with_firebase"
alembic upgrade head
```

### Method 2: マイグレーションファイル修正（データ保持）

既存のデータを保持したい場合の方法です。

#### Step 1: 問題のマイグレーションファイルを特定

```bash
# マイグレーションファイルの確認
ls -la ./alembic/versions/
cat ./alembic/versions/20250220-1645_recreate_all_tables.py
```

#### Step 2: 最初のマイグレーションファイルを修正

**修正前（問題のあるファイル）:**

```python
"""recreate_all_tables

Revision ID: aa3febf53565
Revises: 
Create Date: 2025-02-20 16:45:38.248220
"""

def upgrade():
    op.create_table('users',
        sa.Column('id', sa.UUID(), nullable=False),
        sa.Column('email', sa.String(length=255), nullable=False),
        sa.Column('password', sa.String(length=255), nullable=False),  # 問題：古いスキーマ
        sa.Column('role', sa.Integer(), nullable=False),
        sa.Column('created_at', sa.TIMESTAMP(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=False),
        sa.Column('updated_at', sa.TIMESTAMP(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=False),
        sa.Column('deleted_at', sa.TIMESTAMP(timezone=True), nullable=True),
        sa.PrimaryKeyConstraint('id'),
        sa.UniqueConstraint('email'),
        sa.UniqueConstraint('id')
    )
```

**修正後:**

```python
"""recreate_all_tables

Revision ID: aa3febf53565
Revises: 
Create Date: 2025-02-20 16:45:38.248220
"""

def upgrade():
    op.create_table('users',
        sa.Column('id', sa.UUID(), nullable=False),
        sa.Column('email', sa.String(length=255), nullable=False),
        sa.Column('firebase_uid', sa.String(length=128), nullable=False),  # 修正：新しいスキーマ
        sa.Column('role', sa.Integer(), nullable=False),
        sa.Column('created_at', sa.TIMESTAMP(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=False),
        sa.Column('updated_at', sa.TIMESTAMP(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=False),
        sa.Column('deleted_at', sa.TIMESTAMP(timezone=True), nullable=True),
        sa.PrimaryKeyConstraint('id'),
        sa.UniqueConstraint('email'),
        sa.UniqueConstraint('firebase_uid'),  # 追加：新しい制約
        sa.UniqueConstraint('id')
    )
```

#### Step 3: 重複マイグレーションファイルを削除

```bash
# 後続の重複するマイグレーションファイルを削除
rm ./alembic/versions/20250228-1039_add_img_path_to_profiles.py   # profiles.image_path追加（統合済み）
rm ./alembic/versions/20250331-2233_add_firebase_uid_to_users.py  # firebase_uid追加、password削除（統合済み）
rm ./alembic/versions/20250411-1135_add_user_id_to_trainings.py   # trainings.user_id追加（統合済み）

# 残るのは修正した最初のファイルのみ
ls -la ./alembic/versions/
# 期待される出力：20250220-1645_recreate_all_tables.py のみ
```

#### Step 4: コンテナ再起動とマイグレーション実行

```bash
# 1. 完全停止
docker-compose down

# 2. リビルド付きで起動（マイグレーションファイルの変更を確実に反映）
docker-compose up -d --build

# 3. マイグレーション実行確認
docker exec -it ed_std_web bash
alembic current
alembic upgrade head
```

## 成功の確認方法

### 1. Alembicの状態確認

```bash
docker exec -it ed_std_web bash
alembic current
# 期待される出力: aa3febf53565 (head)

alembic history
# 期待される出力: マイグレーション履歴の一覧
```

### 2. データベース構造の確認

```bash
# データベースコンテナに入る
docker exec -it ed_std_db psql -U docker -d baseball_note

# usersテーブルの構造確認
\d users

# 全テーブルの一覧確認
\dt

# 終了
\q
```

**期待されるusersテーブル構造:**

```
                    Table "public.users"
   Column    |           Type           | Collation | Nullable | Default 
-------------+--------------------------+-----------+----------+---------
 id          | uuid                     |           | not null | 
 firebase_uid| character varying(128)   |           | not null | 
 email       | character varying(255)   |           | not null | 
 role        | integer                  |           | not null | 
 created_at  | timestamp with time zone |           | not null | CURRENT_TIMESTAMP
 updated_at  | timestamp with time zone |           | not null | CURRENT_TIMESTAMP
 deleted_at  | timestamp with time zone |           |          | 

Indexes:
    "users_pkey" PRIMARY KEY, btree (id)
    "users_email_key" UNIQUE CONSTRAINT, btree (email)
    "users_firebase_uid_key" UNIQUE CONSTRAINT, btree (firebase_uid)
```

### 3. APIの動作確認

```bash
# APIサーバーの起動確認
curl http://localhost:8080/

# 期待される出力例
# {"title": "FastAPI Application", "version": "1.0.0"}
```

## よくある追加の問題と対処法

### Seedファイルのエラー

マイグレーション成功後に以下のエラーが出ることがあります：

```bash
seeds import failed. detail=type object 'Users' has no attribute 'password'
```

**原因:** テストデータ（seedファイル）が古いpasswordフィールドを参照している

**対処法:** seeder/seeds_json/users.jsonファイルを更新

```json
// 修正前
{
  "email": "test@example.com",
  "password": "hashedpassword",
  "role": 0
}
```

```json
// 修正後
{
  "email": "test@example.com",
  "firebase_uid": "firebase_test_uid_123",
  "role": 0
}
```

### test_dbのヘルスチェックエラー

```yaml
# docker-compose.ymlのtest_db設定を修正
test_db:
  healthcheck:
    test: ["CMD-SHELL", "pg_isready -U test -d test"]  # -d test を追加
```

## 予防策

### 1. 段階的なマイグレーション作成

大きなスキーマ変更は段階的に行う：

```bash
# 悪い例：一度に大きな変更
alembic revision --autogenerate -m "replace_password_with_firebase_uid"

# 良い例：段階的な変更
alembic revision --autogenerate -m "add_firebase_uid_column"
alembic revision --autogenerate -m "remove_password_column"
```

### 2. マイグレーション作成前の確認

```bash
# 現在の状態を確認
alembic current
alembic history

# モデルとマイグレーションの整合性確認
alembic check
```

### 3. 開発環境での安全な再起動

```bash
# データを保持してコンテナのみ再起動
docker-compose restart

# 完全リビルドが必要な場合
docker-compose down
docker-compose up -d --build  # ボリュームは保持される

# データも完全にリセットする場合
docker-compose down -v
docker-compose up -d --build
```

### 4. チーム開発での注意点

- マイグレーション変更は必ずチーム全体で共有
- 本番環境では絶対にMethod 1（完全リセット）を使用しない  
- 本番データがある場合は必ずバックアップを取る
- マイグレーションのテストは開発環境で十分に行う

## まとめ

このエラーは Docker + Alembic の組み合わせでよく発生する問題ですが、適切な手順で確実に解決できます。

**開発環境の場合:**
- Method 1（完全リセット）が最も確実で簡単

**本番環境の場合:**
- Method 2（マイグレーションファイル修正）でデータを保持
- 必ずバックアップを取ってから実行

**重要なポイント:**
- マイグレーションファイルとモデル定義の整合性を保つ
- 段階的な変更を心がける
- チーム開発では変更内容を必ず共有

この記事が同様の問題に直面している方の助けになれば幸いです。

## 参考情報

- [Alembic公式ドキュメント](https://alembic.sqlalchemy.org/)
- [FastAPI公式ドキュメント](https://fastapi.tiangolo.com/)
- [SQLAlchemy公式ドキュメント](https://docs.sqlalchemy.org/)