データベースのマイグレーションの作成方法

このドキュメントでは、遭遇する可能性のあるいくつかのシナリオに対する、データベースのマイグレーションの構成方法と書き方について説明します。マイグレーションに関する入門的な資料を探しているなら、トピック別ガイド を読んでください。

データのマイグレーションと複数のデータベース

複数のデータベースを使用している場合、特定のデータベースに対してマイグレーションを実行するかどうかを判断する必要があるかもしれません。例えば、特定のデータベースに対してのみマイグレーションを実行したい場合です。

これを行うには、 RunPython 操作の中で、 schema_editor.connection.alias 属性を見て、データベース接続のエイリアスを確認します:

from django.db import migrations


def forwards(apps, schema_editor):
    if schema_editor.connection.alias != "default":
        return
    # Your migration code goes here


class Migration(migrations.Migration):
    dependencies = [
        # Dependencies to other migrations
    ]

    operations = [
        migrations.RunPython(forwards),
    ]

また、データベースルーターの allow_migrate() メソッドに渡すヒントを **hints として指定することもできます:

myapp/dbrouters.py
class MyRouter:
    def allow_migrate(self, db, app_label, model_name=None, **hints):
        if "target_db" in hints:
            return db == hints["target_db"]
        return True

これをマイグレーションに利用するには、次のようにします:

from django.db import migrations


def forwards(apps, schema_editor):
    # Your migration code goes here
    ...


class Migration(migrations.Migration):
    dependencies = [
        # Dependencies to other migrations
    ]

    operations = [
        migrations.RunPython(forwards, hints={"target_db": "default"}),
    ]

もし RunPythonRunSQL 操作が 1 つのモデルにしか影響しないのであれば、 model_name をヒントとして渡すことで、ルーターに対して最大限、透過的になります。これは再利用可能なサードパーティアプリにとって特に重要です。

ユニークなフィールドを追加するマイグレーション

既存の行を持つテーブルにユニークな非 Null フィールドを追加する「プレーン」なマイグレーションを適用するとエラーが発生します。これは、既存の行すべてを埋めるために使用される値がただ一つだけ生成され、これがユニーク制約に違反するためです。

そのため、次の手順を踏む必要があります。この例では、デフォルト値を持つ非 Null の UUIDField を追加します。必要に応じて、該当するフィールドを変更してください。

  • モデルに default=uuid.uuid4unique=True 引数を持つフィールドを追加します(追加するフィールドのタイプに適切なデフォルトを選択してください)。

  • makemigrations コマンドを実行します。これにより、 AddField 操作を含むマイグレーションが生成されます。

  • 同じアプリに対して makemigrations myapp --empty を2回実行して、2つの空のマイグレーションファイルを生成します。以下の例では、マイグレーションファイルにわかりやすい名前を付けるためにファイル名を変更しています。

  • 自動生成されたマイグレーション(3つの新しいファイルのうちの最初のファイル)から AddField オペレーションを最後のマイグレーションにコピーし、 AddFieldAlterField に変更し、 uuidmodels のインポートを追加します。例:

    0006_remove_uuid_null.py
    # Generated by Django A.B on YYYY-MM-DD HH:MM
    from django.db import migrations, models
    import uuid
    
    
    class Migration(migrations.Migration):
        dependencies = [
            ("myapp", "0005_populate_uuid_values"),
        ]
    
        operations = [
            migrations.AlterField(
                model_name="mymodel",
                name="uuid",
                field=models.UUIDField(default=uuid.uuid4, unique=True),
            ),
        ]
    
  • 最初の移行ファイルを編集します。 生成された移行クラスは次のようになります:

    0004_add_uuid_field.py
    class Migration(migrations.Migration):
        dependencies = [
            ("myapp", "0003_auto_20150129_1705"),
        ]
    
        operations = [
            migrations.AddField(
                model_name="mymodel",
                name="uuid",
                field=models.UUIDField(default=uuid.uuid4, unique=True),
            ),
        ]
    

    unique=Truenull=True に変更してください。これは、一時的にNULLフィールドを作成し、すべての行に一意な値を設定するまで、一意制約の作成を延期します。

  • 最初の空のマイグレーションファイルに、 RunPython または RunSQL オペレーションを追加して、既存の各行に対して一意な値 (この例では UUID) を生成します。 uuid のインポートも追加します。例:

    0005_populate_uuid_values.py
    # Generated by Django A.B on YYYY-MM-DD HH:MM
    from django.db import migrations
    import uuid
    
    
    def gen_uuid(apps, schema_editor):
        MyModel = apps.get_model("myapp", "MyModel")
        for row in MyModel.objects.all():
            row.uuid = uuid.uuid4()
            row.save(update_fields=["uuid"])
    
    
    class Migration(migrations.Migration):
        dependencies = [
            ("myapp", "0004_add_uuid_field"),
        ]
    
        operations = [
            # omit reverse_code=... if you don't want the migration to be reversible.
            migrations.RunPython(gen_uuid, reverse_code=migrations.RunPython.noop),
        ]
    
  • これで migrate コマンドを使って通常通りマイグレーションを適用することができます。

    このマイグレーションの実行中にオブジェクトの作成を許可すると、競合状態が発生することに注意してください。 AddField の後でかつ RunPython の前に作成されたオブジェクトは、元の uuid が上書きされます。

非アトミックのマイグレーション

DDL トランザクションをサポートしているデータベース (SQLite と PostgreSQL) では、マイグレーションはデフォルトでトランザクション内で実行されます。大きなテーブルに対してデータ移行を行う場合などには、 atomic 属性を False に設定することで、トランザクション内で移行が実行されないようにすることができます:

from django.db import migrations


class Migration(migrations.Migration):
    atomic = False

このようなマイグレーションでは、すべての操作はトランザクションなしで実行されます。 atomic() を使用するか、 RunPythonatomic=True を渡すことで、マイグレーションの一部をトランザクション内で実行できます。

以下は、大きなテーブルを小さなバッチで更新する、非アトミックなデータマイグレーションの例です:

import uuid

from django.db import migrations, transaction


def gen_uuid(apps, schema_editor):
    MyModel = apps.get_model("myapp", "MyModel")
    while MyModel.objects.filter(uuid__isnull=True).exists():
        with transaction.atomic():
            for row in MyModel.objects.filter(uuid__isnull=True)[:1000]:
                row.uuid = uuid.uuid4()
                row.save()


class Migration(migrations.Migration):
    atomic = False

    operations = [
        migrations.RunPython(gen_uuid),
    ]

atomic 属性は DDL トランザクションをサポートしていないデータベース (MySQL や Oracle など) には影響しません。(MySQLの atomic DDL statement support は、ロールバック可能なトランザクションに包まれた複数のステートメントではなく、個々のステートメントを指します)。

マイグレーションの順序をコントロールする

Django はマイグレーションを適用する順番を、マイグレーションのファイル名ではなく、 Migration クラスの 2 つのプロパティである dependenciesrun_before を使ってグラフを作成することで決定します:

makemigrations コマンドを使ったことがあるなら、おそらく dependencies の動作をすでに見ているでしょう。自動生成されたマイグレーションは作成プロセスの一部としてそれを定義するためです。

dependencies プロパティは次のように宣言されます:

from django.db import migrations


class Migration(migrations.Migration):
    dependencies = [
        ("myapp", "0123_the_previous_migration"),
    ]

通常はこれで十分ですが、時にはあなたのマイグレーションが他のマイグレーションよりも 先に 実行されるようにする必要があるかもしれません。これは例えば、サードパーティアプリのマイグレーションを AUTH_USER_MODEL の置換の 後に 実行させるのに便利です。

これを行うには、 Migration クラスの run_before 属性に、依存するすべてのマイグレーションを配置します:

class Migration(migrations.Migration):
    ...

    run_before = [
        ("third_party_app", "0001_do_awesome"),
    ]

可能な限り、 run_before よりも dependencies を使うことを推奨します。 run_before を使うのは、書いているマイグレーションの後に実行したいマイグレーションで dependencies を指定するのが望ましくないか、現実的でない場合のみです。

サードパーティのアプリ間でデータをマイグレーションする

データマイグレーションを使って、あるサードパーティアプリケーションから別のアプリケーションにデータを移動できます。

後で古いアプリを削除する予定がある場合は、古いアプリがインストールされているかどうかに基づいて dependencies プロパティを設定する必要があります。そうしないと、古いアプリをアンインストールしたときに依存関係がなくなってしまいます。同様に、古いアプリからモデルを取得する apps.get_model() の呼び出しで LookupError をキャッチする必要があります。この方法によって、古いアプリをインストールしてからアンインストールすることなく、プロジェクトをどこにでもデプロイできるようになります。

簡単なマイグレーションの例を見てみましょう:

myapp/migrations/0124_move_old_app_to_new_app.py
from django.apps import apps as global_apps
from django.db import migrations


def forwards(apps, schema_editor):
    try:
        OldModel = apps.get_model("old_app", "OldModel")
    except LookupError:
        # The old app isn't installed.
        return

    NewModel = apps.get_model("new_app", "NewModel")
    NewModel.objects.bulk_create(
        NewModel(new_attribute=old_object.old_attribute)
        for old_object in OldModel.objects.all()
    )


class Migration(migrations.Migration):
    operations = [
        migrations.RunPython(forwards, migrations.RunPython.noop),
    ]
    dependencies = [
        ("myapp", "0123_the_previous_migration"),
        ("new_app", "0001_initial"),
    ]

    if global_apps.is_installed("old_app"):
        dependencies.append(("old_app", "0001_initial"))

また、マイグレーションが適用されないときにどうしたいかも検討します。(上記の例のように)何もしないか、新しいアプリケーションからデータの一部または全部を削除できます。 それに応じて RunPython 操作の2番目の引数を調整します。

ManyToManyField を中間 (through) モデルを使うように変更する

ManyToManyFieldthrough モデルに変更すると、デフォルトのマイグレーションは既存のテーブルを削除して新しいテーブルを作成し、既存のリレーションを失います。これを避けるには、SeparateDatabaseAndState を使用して、マイグレーションの自動検出器に新しいモデルが作成されたことを伝えながら、既存のテーブル名を新しいテーブル名に変更します。既存のテーブル名は sqlmigrate または dbshell で確認できます。新しいテーブル名は中間モデルの _meta.db_table プロパティで確認できます。新しい through モデルは ForeignKey に Django と同じ名前を使うべきです。また、追加のフィールドが必要な場合は、 SeparateDatabaseAndState の後の操作で追加してください。

例えば、 Author にリンクする ManyToManyField を持つ Book モデルがあった場合、次のように新しいフィールド is_primary を持つ中間モデル AuthorBook を追加できます:

from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):
    dependencies = [
        ("core", "0001_initial"),
    ]

    operations = [
        migrations.SeparateDatabaseAndState(
            database_operations=[
                # Old table name from checking with sqlmigrate, new table
                # name from AuthorBook._meta.db_table.
                migrations.RunSQL(
                    sql="ALTER TABLE core_book_authors RENAME TO core_authorbook",
                    reverse_sql="ALTER TABLE core_authorbook RENAME TO core_book_authors",
                ),
            ],
            state_operations=[
                migrations.CreateModel(
                    name="AuthorBook",
                    fields=[
                        (
                            "id",
                            models.AutoField(
                                auto_created=True,
                                primary_key=True,
                                serialize=False,
                                verbose_name="ID",
                            ),
                        ),
                        (
                            "author",
                            models.ForeignKey(
                                on_delete=django.db.models.deletion.DO_NOTHING,
                                to="core.Author",
                            ),
                        ),
                        (
                            "book",
                            models.ForeignKey(
                                on_delete=django.db.models.deletion.DO_NOTHING,
                                to="core.Book",
                            ),
                        ),
                    ],
                ),
                migrations.AlterField(
                    model_name="book",
                    name="authors",
                    field=models.ManyToManyField(
                        to="core.Author",
                        through="core.AuthorBook",
                    ),
                ),
            ],
        ),
        migrations.AddField(
            model_name="authorbook",
            name="is_primary",
            field=models.BooleanField(default=False),
        ),
    ]

未管理状態のモデルを管理状態に変更する

未管理状態のモデル(managed=False) を管理状態に変更する場合、 managed=False を削除し、マイグレーションを生成してから、モデルにスキーマ関連の変更を加える必要があります。これは、 Meta.managed を変更する操作を含むマイグレーションに含まれるスキーマ変更が適用されない可能性があるためです。

Back to Top