2.

Django マイグレーションファイル作成完全ガイド

編集
この記事の要点
  • python manage.py makemigrations [appname] でモデル変更からマイグレーションファイルを自動生成
  • ファイルは myapp/migrations/0001_initial.py のように番号付きで保存される
  • Operations: CreateModel / AddField / AlterField / RenameField / DeleteModel / RunSQL / RunPython
  • dependencies で他アプリ・他バージョンへの依存を表現。Django が DAG として実行順を解決
  • squashmigrations で過去のマイグレーションをまとめる。migrate --fake で適用扱いだけ記録
  • 本番デプロイ時の注意: NULL 不可列追加は 2 段階で / カラム削除は段階リリース / RunPython は元に戻せる関数も書く

基本フロー

# 1. models.py を編集
# 2. マイグレーション生成
python manage.py makemigrations
# Migrations for 'myapp':
#   myapp/migrations/0002_article_published_at.py
#     - Add field published_at to article

# 3. SQL を確認(実行しない)
python manage.py sqlmigrate myapp 0002

# 4. 適用
python manage.py migrate

# 5. 状態確認
python manage.py showmigrations

生成されたファイルの中身

# myapp/migrations/0002_article_published_at.py
from django.db import migrations, models

class Migration(migrations.Migration):

    dependencies = [
        ('myapp', '0001_initial'),     # 自アプリの前バージョン
        ('auth', '0012_alter_user_first_name_max_length'),  # 他アプリ依存
    ]

    operations = [
        migrations.AddField(
            model_name='article',
            name='published_at',
            field=models.DateTimeField(null=True, blank=True),
        ),
    ]

主な Operation

Operation用途
CreateModelテーブル新規作成
DeleteModelテーブル削除
RenameModelテーブル名変更
AddField列追加
RemoveField列削除
AlterField列の定義変更(型・制約)
RenameField列名変更
AddIndex / RemoveIndexインデックス追加・削除
AddConstraint / RemoveConstraint制約追加・削除
AlterModelOptionsMeta オプション変更
RunSQL生 SQL 実行
RunPythonPython 関数実行(データ移行)

データ移行 (RunPython)

列追加とセットでデータの埋め直しが必要な場合:

# 空のマイグレーションを生成
python manage.py makemigrations myapp --empty -n populate_slug
# 0003_populate_slug.py
from django.db import migrations
from django.utils.text import slugify

def forwards(apps, schema_editor):
    Article = apps.get_model('myapp', 'Article')   # 履歴モデルを使う
    for a in Article.objects.all():
        if not a.slug:
            a.slug = slugify(a.title)
            a.save(update_fields=['slug'])

def backwards(apps, schema_editor):
    # 元に戻す処理(空にするなど)
    Article = apps.get_model('myapp', 'Article')
    Article.objects.update(slug='')

class Migration(migrations.Migration):
    dependencies = [('myapp', '0002_article_slug')]
    operations = [
        migrations.RunPython(forwards, backwards),
    ]

重要: RunPython 内で from myapp.models import Article と直接 import するのは NG。apps.get_model() でその時点のモデル定義を取り出すこと(将来モデルが変わってもマイグレーションが壊れない)。

生 SQL: RunSQL

class Migration(migrations.Migration):
    dependencies = [('myapp', '0003_populate_slug')]
    operations = [
        migrations.RunSQL(
            sql="CREATE INDEX CONCURRENTLY idx_article_slug ON myapp_article(slug);",
            reverse_sql="DROP INDEX IF EXISTS idx_article_slug;",
        ),
    ]

本番デプロイの定石

1. NOT NULL 列を追加するときは 2 段階

# Step 1: nullable で追加 + デフォルト値で埋める
class Article(models.Model):
    status = models.CharField(max_length=20, null=True, blank=True)
# → makemigrations / migrate

# Step 2: データを埋める(RunPython)
# → makemigrations --empty で生成

# Step 3: NOT NULL に変更
class Article(models.Model):
    status = models.CharField(max_length=20, default='draft')
# → makemigrations / migrate

2. 列削除も段階リリース

  1. アプリコードからその列の参照を全部消す(古い Pod でも動く状態)
  2. デプロイして全 Pod が新コードに切替
  3. 次のリリースで RemoveField

3. 名前変更 (RenameField)

Django は新旧 2 列が「同名・別フィールド」か「列名変更」か判別できないため、makemigrations 実行時に対話で確認されます。CI で自動化するなら -n オプションで名前付ける。

squashmigrations

マイグレーションが 100 枚溜まって遅い場合、まとめられます:

# 0001 〜 0050 を 1 つにまとめる
python manage.py squashmigrations myapp 0001 0050
# → 0001_squashed_0050_xxx.py が生成される

# 全環境が squashed を適用済になったら、古い 0001〜0050 を削除可能

migrate --fake / fake-initial

# 既存 DB に対し「適用したことにする」(実 SQL は流さない)
python manage.py migrate myapp 0003 --fake

# 既存テーブルが存在する場合、CreateModel 部分だけ fake
python manage.py migrate --fake-initial

# 戻す
python manage.py migrate myapp 0002    # 0002 まで戻る(逆操作実行)
python manage.py migrate myapp zero    # 全部戻す

マイグレーションが壊れたとき

# 履歴確認
python manage.py showmigrations
# [X] 0001_initial
# [X] 0002_article_slug
# [ ] 0003_populate_slug    ← 未適用

# 履歴を直接見る
SELECT app, name, applied FROM django_migrations WHERE app='myapp' ORDER BY applied;

# 履歴を強制的に書き換え(最終手段)
DELETE FROM django_migrations WHERE app='myapp' AND name='0003_populate_slug';
# その後 migrate --fake で適用扱いにするなど

CI / コードレビューの観点

  • マイグレーションファイルは 必ず Git にコミット。アプリコードと同じ PR で
  • showmigrations --plan で適用順をレビュー
  • 大規模本番では --plan + sqlmigrate で SQL を全部確認
  • ロックを取る DDL(ALTER TABLE 等)は夜間メンテ枠
  • PostgreSQL は CONCURRENTLY 系操作を RunSQL で書き、atomic = False を指定

FAQ

Q: makemigrations が「No changes detected」と言う
A: アプリが INSTALLED_APPS に登録されていない、または models.py を保存し忘れ。python manage.py makemigrations myapp とアプリ名を明示してみる。

Q: マイグレーションを取り消したい
A: migrate myapp NNNN で過去の番号まで戻れる。migrate myapp zero でそのアプリの全マイグレーションを巻き戻し。

Q: 履歴をリセットしたい(開発環境)
A: migrations/ 配下の 0001 以外を削除 → DB を作り直し → makemigrations + migrate。本番では絶対やらない。

編集
Post Share
子ページ

子ページはありません

同階層のページ
  1. Model の定義方法
  2. マイグレーションファイルの作成
  3. テーブル定義の確認
  4. テーブルの作成
  5. テーブル名 = アプリケーション名 + モデル名の設定変更
  6. モデルの中身を確認