データベース移行の記述—Djangoドキュメント

提供:Dev Guides
< DjangoDjango/docs/3.2.x/howto/writing-migrations
移動先:案内検索

データベース移行の記述

このドキュメントでは、発生する可能性のあるさまざまなシナリオのデータベース移行を構造化および作成する方法について説明します。 移行の紹介資料については、トピックガイドを参照してください。

データ移行と複数のデータベース

複数のデータベースを使用する場合、特定のデータベースに対して移行を実行するかどうかを判断する必要がある場合があります。 たとえば、特定のデータベースでのみの移行を実行したい場合があります。

これを行うには、schema_editor.connection.alias属性を調べて、RunPython操作内のデータベース接続のエイリアスを確認できます。

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'}),
    ]

RunPythonまたはRunSQL操作が1つのモデルにのみ影響する場合は、model_nameをヒントとして渡して、ルーターに対して可能な限り透過的にすることをお勧めします。 これは、再利用可能なサードパーティのアプリにとって特に重要です。


一意のフィールドを追加する移行

既存の行を含むテーブルに一意のNULL不可フィールドを追加する「プレーン」移行を適用すると、既存の行の入力に使用される値が1回だけ生成され、一意の制約が破られるため、エラーが発生します。

したがって、次の手順を実行する必要があります。 この例では、null許容でない UUIDField をデフォルト値で追加します。 必要に応じて、それぞれのフィールドを変更します。

  • default=uuid.uuid4およびunique=True引数を使用してモデルにフィールドを追加します(追加するフィールドのタイプに適切なデフォルトを選択します)。

  • :djadmin: `makemigrations` コマンドを実行します。 これにより、AddField操作で移行が生成されます。

  • makemigrations myapp --emptyを2回実行して、同じアプリの2つの空の移行ファイルを生成します。 以下の例では、移行ファイルの名前を変更して、意味のある名前を付けています。

  • AddField操作を自動生成された移行(3つの新しいファイルの最初)から最後の移行にコピーし、AddFieldAlterFieldに変更し、[X171Xのインポートを追加します] およびmodels。 例えば:

    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),
        ]
  • これで、:djadmin: `migrate` コマンドを使用して通常どおり移行を適用できます。

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

非原子移動

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

from django.db import migrations

class Migration(migrations.Migration):
    atomic = False

このような移行では、すべての操作がトランザクションなしで実行されます。 atomic()を使用するか、atomic=TrueRunPythonに渡すことにより、トランザクション内で移行の一部を実行できます。

大きなテーブルを小さなバッチで更新する非アトミックデータ移行の例を次に示します。

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のアトミックDDLステートメントサポートは、ロールバック可能なトランザクションにラップされた複数のステートメントではなく、個々のステートメントを参照します。)


移行の順序を制御する

Djangoは、各移行のファイル名ではなく、Migrationクラスの2つのプロパティdependenciesrun_beforeを使用してグラフを作成することにより、移行を適用する順序を決定します。

:djadmin: `makemigrations` コマンドを使用したことがある場合、自動作成された移行では作成プロセスの一部として定義されているため、dependenciesの動作をすでに確認しているはずです。

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

from django.db import migrations

class Migration(migrations.Migration):

    dependencies = [
        ('myapp', '0123_the_previous_migration'),
    ]

通常はこれで十分ですが、場合によっては、他の移行の前に実行する必要があります。 これは、たとえば、サードパーティアプリの移行を ' の後に:setting: `AUTH_USER_MODEL` 置換で実行する場合に役立ちます。

これを実現するには、自分に依存する必要のあるすべての移行を、Migrationクラスのrun_before属性に配置します。

class Migration(migrations.Migration):
    ...

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

可能であれば、run_beforeよりもdependenciesを使用することをお勧めします。 作成している移行の後に実行する移行でdependenciesを指定することが望ましくないか、実用的でない場合にのみ、run_beforeを使用する必要があります。


サードパーティアプリ間でのデータの移行

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

後で古いアプリを削除する場合は、古いアプリがインストールされているかどうかに基づいて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 を使用して、新しいモデルが作成されたことを移行自動検出器に通知しながら、既存のテーブルの名前を新しいテーブル名に変更できます。 既存のテーブル名は、:djadmin: `sqlmigrate` または:djadmin:` dbshell` で確認できます。 スルーモデルの_meta.db_tableプロパティを使用して、新しいテーブル名を確認できます。 新しいthroughモデルでは、ForeignKeyにDjangoと同じ名前を使用する必要があります。 また、追加のフィールドが必要な場合は、 SeparateDatabaseAndState の後の操作に追加する必要があります。

たとえば、ManyToManyFieldAuthorにリンクしているBookモデルがある場合、新しいフィールドis_primary、そのように:

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は適用されない場合があります。