データベースのマイグレーションの作成方法¶
このドキュメントでは、遭遇する可能性のあるいくつかのシナリオに対する、データベースのマイグレーションの構成方法と書き方について説明します。マイグレーションに関する入門的な資料を探しているなら、トピック別ガイド を読んでください。
データのマイグレーションと複数のデータベース¶
複数のデータベースを使用している場合、特定のデータベースに対してマイグレーションを実行するかどうかを判断する必要があるかもしれません。例えば、特定のデータベースに対してのみマイグレーションを実行したい場合です。
これを行うには、 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 ForeignKey
s 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
を変更する操作を含むマイグレーションに含まれるスキーマ変更が適用されない可能性があるためです。