データベースのマイグレーションの作成方法¶
このドキュメントでは、遭遇する可能性のあるいくつかのシナリオに対する、データベースのマイグレーションの構成方法と書き方について説明します。マイグレーションに関する入門的な資料を探しているなら、トピック別ガイド を読んでください。
データのマイグレーションと複数のデータベース¶
複数のデータベースを使用している場合、特定のデータベースに対してマイグレーションを実行するかどうかを判断する必要があるかもしれません。例えば、特定のデータベースに対してのみマイグレーションを実行したい場合です。
これを行うには、 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"}),
]
もし RunPython や RunSQL 操作が 1 つのモデルにしか影響しないのであれば、 model_name をヒントとして渡すことで、ルーターに対して最大限、透過的になります。これは再利用可能なサードパーティアプリにとって特に重要です。
ユニークなフィールドを追加するマイグレーション¶
既存の行を持つテーブルにユニークな非 Null フィールドを追加する「プレーン」なマイグレーションを適用するとエラーが発生します。これは、既存の行すべてを埋めるために使用される値がただ一つだけ生成され、これがユニーク制約に違反するためです。
そのため、次の手順を踏む必要があります。この例では、デフォルト値を持つ非 Null の UUIDField を追加します。必要に応じて、該当するフィールドを変更してください。
モデルに
default=uuid.uuid4とunique=True引数を持つフィールドを追加します(追加するフィールドのタイプに適切なデフォルトを選択してください)。makemigrationsコマンドを実行します。これにより、AddField操作を含むマイグレーションが生成されます。同じアプリに対して
makemigrations myapp --emptyを2回実行して、2つの空のマイグレーションファイルを生成します。以下の例では、マイグレーションファイルにわかりやすい名前を付けるためにファイル名を変更しています。自動生成されたマイグレーション(3つの新しいファイルのうちの最初のファイル)から
AddFieldオペレーションを最後のマイグレーションにコピーし、AddFieldをAlterFieldに変更し、uuidと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=Trueをnull=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() を使用するか、 RunPython に atomic=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 つのプロパティである dependencies と run_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) モデルを使うように変更する¶
If you change a ManyToManyField to use a through
model, the default migration will delete the existing table and create a new
one, losing the existing relations. To avoid this, you can use
SeparateDatabaseAndState to rename the existing table to the new
table name while telling the migration autodetector that the new model has
been created. You can check the existing table name and constraint name through
sqlmigrate or dbshell. You can check the new table name
with the through model's _meta.db_table property. Your new through
model should use the same names for the ForeignKeys as Django did. Also if
it needs any extra fields, they should be added in operations after
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",
),
),
],
options={
"constraints": [
models.UniqueConstraint(
fields=["author", "book"],
name="unique_author_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 を変更する操作を含むマイグレーションに含まれるスキーマ変更が適用されない可能性があるためです。