データベース移行の記述
このドキュメントでは、発生する可能性のあるさまざまなシナリオのデータベース移行を構造化および作成する方法について説明します。 移行の紹介資料については、トピックガイドを参照してください。
データ移行と複数のデータベース
複数のデータベースを使用する場合、特定のデータベースに対して移行を実行するかどうかを判断する必要がある場合があります。 たとえば、特定のデータベースでのみの移行を実行したい場合があります。
これを行うには、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
として提供することもできます。
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つの新しいファイルの最初)から最後の移行にコピーし、AddField
をAlterField
に変更し、[X171Xのインポートを追加します] およびmodels
。 例えば:# 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), ), ]
最初の移行ファイルを編集します。 生成された移行クラスは次のようになります。
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=True
をnull=True
に変更します。これにより、中間nullフィールドが作成され、すべての行に一意の値が入力されるまで、一意の制約の作成が延期されます。最初の空の移行ファイルで、 RunPython または RunSQL 操作を追加して、既存の行ごとに一意の値(例ではUUID)を生成します。
uuid
のインポートも追加します。 例えば:# 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=True
をRunPython
に渡すことにより、トランザクション内で移行の一部を実行できます。
大きなテーブルを小さなバッチで更新する非アトミックデータ移行の例を次に示します。
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つのプロパティdependencies
とrun_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
をキャッチする必要があります。 このアプローチにより、最初に古いアプリをインストールしてからアンインストールしなくても、プロジェクトをどこにでもデプロイできます。
移行の例は次のとおりです。
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モデルを使用するように変更する
ManyToManyField をthrough
モデルを使用するように変更すると、デフォルトの移行では既存のテーブルが削除されて新しいテーブルが作成され、既存のリレーションが失われます。 これを回避するには、 SeparateDatabaseAndState を使用して、新しいモデルが作成されたことを移行自動検出器に通知しながら、既存のテーブルの名前を新しいテーブル名に変更できます。 既存のテーブル名は、:djadmin: `sqlmigrate` または:djadmin:` dbshell` で確認できます。 スルーモデルの_meta.db_table
プロパティを使用して、新しいテーブル名を確認できます。 新しいthrough
モデルでは、ForeignKey
にDjangoと同じ名前を使用する必要があります。 また、追加のフィールドが必要な場合は、 SeparateDatabaseAndState の後の操作に追加する必要があります。
たとえば、ManyToManyField
がAuthor
にリンクしている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
は適用されない場合があります。