Så här skapar du databasmigreringar¶
Detta dokument förklarar hur man strukturerar och skriver databasmigreringar för olika scenarier som du kan stöta på. För inledande material om migreringar, se the topic guide.
Datamigreringar och flera databaser¶
När du använder flera databaser kan du behöva ta reda på om du ska köra en migrering mot en viss databas eller inte. Du kanske till exempel vill köra en migrering endast på en viss databas.
För att göra det kan du kontrollera databasanslutningens alias i en RunPython
-operation genom att titta på attributet 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),
]
Du kan också ange tips som kommer att skickas till allow_migrate()
-metoden för databasroutrar som **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
Gör sedan följande för att utnyttja detta i dina migreringar:
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"}),
]
Om din RunPython
eller RunSQL
operation bara påverkar en modell är det god praxis att skicka model_name
som en ledtråd för att göra det så transparent som möjligt för routern. Detta är särskilt viktigt för återanvändbara appar och tredjepartsappar.
Migreringar som lägger till unika fält¶
Om du använder en ”vanlig” migrering som lägger till ett unikt icke-nullbart fält i en tabell med befintliga rader kommer det att uppstå ett fel eftersom det värde som används för att fylla i befintliga rader genereras endast en gång, vilket bryter den unika begränsningen.
Därför bör följande steg vidtas. I det här exemplet lägger vi till ett icke-nullbart UUIDField
med ett standardvärde. Modifiera respektive fält efter dina behov.
Lägg till fältet i din modell med argumenten
default=uuid.uuid4
ochunique=True
(välj en lämplig standard för den typ av fält du lägger till).Kör kommandot
makemigrations
. Detta bör generera en migrering med enAddField
operation.Generera två tomma migreringsfiler för samma app genom att köra
makemigrations myapp --empty
två gånger. Vi har döpt om migreringsfilerna så att de får meningsfulla namn i exemplen nedan.Kopiera åtgärden
AddField
från den autogenererade migreringen (den första av de tre nya filerna) till den sista migreringen, ändraAddField
tillAlterField
och lägg till import avuuid
ochmodels
. Ett exempel: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), ), ]
Redigera den första migreringsfilen. Den genererade migreringsklassen bör se ut ungefär så här:
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), ), ]
Ändra
unique=True
tillnull=True
- detta kommer att skapa det mellanliggande null-fältet och skjuta upp skapandet av den unika begränsningen tills vi har fyllt i unika värden på alla rader.I den första tomma migreringsfilen lägger du till en
RunPython
ellerRunSQL
operation för att generera ett unikt värde (UUID i exemplet) för varje befintlig rad. Lägg också till en import avuuid
. För att ta ett exempel: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), ]
Nu kan du tillämpa migreringarna som vanligt med kommandot
migrate
.Observera att det finns ett tävlingsförhållande om du tillåter att objekt skapas medan denna migrering körs. Objekt som skapas efter
AddField
och föreRunPython
kommer att få sina ursprungligauuid
överskrivna.
Icke-atomiska migreringar¶
På databaser som stöder DDL-transaktioner (SQLite och PostgreSQL) kommer migreringar att köras i en transaktion som standard. För användningsfall som att utföra datamigreringar på stora tabeller kanske du vill förhindra att en migrering körs i en transaktion genom att ställa in attributet atomic
till False
:
from django.db import migrations
class Migration(migrations.Migration):
atomic = False
Inom en sådan migrering körs alla operationer utan en transaktion. Det är möjligt att köra delar av migreringen inuti en transaktion med atomic()
eller genom att skicka atomic=True
till RunPython
.
Här är ett exempel på en icke-atomisk datamigrering som uppdaterar en stor tabell i mindre satser:
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),
]
Attributet atomic
har ingen effekt på databaser som inte stöder DDL-transaktioner (t.ex. MySQL, Oracle). (MySQL:s atomic DDL statement support hänvisar till enskilda uttalanden snarare än flera uttalanden som är insvepta i en transaktion som kan rullas tillbaka)
Kontroll av migrationsordningen¶
Django bestämmer i vilken ordning migreringar ska tillämpas, inte genom filnamnet för varje migrering, utan genom att bygga en graf med hjälp av två egenskaper för klassen Migration
: dependencies
och run_before
.
Om du har använt kommandot makemigrations
har du förmodligen redan sett dependencies
i aktion eftersom automatiskt skapade migreringar har detta definierat som en del av deras skapandeprocess.
Egenskapen dependencies
deklareras på följande sätt:
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("myapp", "0123_the_previous_migration"),
]
Vanligtvis räcker det, men ibland kan du behöva se till att din migrering körs före andra migreringar. Detta är till exempel användbart för att få migreringar av tredjepartsappar att köras efter din AUTH_USER_MODEL
-ersättning.
För att uppnå detta, placera alla migreringar som bör bero på din i run_before
-attributet på din Migration
-klass:
class Migration(migrations.Migration):
...
run_before = [
("third_party_app", "0001_do_awesome"),
]
Använd hellre dependencies
än run_before
när det är möjligt. Du bör endast använda run_before
om det inte är önskvärt eller opraktiskt att ange dependencies
i den migrering som du vill köra efter den du skriver.
Migrering av data mellan appar från tredje part¶
Du kan använda en datamigrering för att flytta data från en tredjepartsapplikation till en annan.
Om du planerar att ta bort den gamla appen senare måste du ställa in egenskapen dependencies
baserat på om den gamla appen är installerad eller inte. Annars kommer du att sakna beroenden när du avinstallerar den gamla appen. På samma sätt måste du fånga LookupError
i anropet apps.get_model()
som hämtar modeller från den gamla appen. Med det här tillvägagångssättet kan du distribuera ditt projekt var som helst utan att först installera och sedan avinstallera den gamla appen.
Här är ett exempel på en migrering:
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"))
Fundera också på vad du vill ska hända när migreringen inte tillämpas. Du kan antingen inte göra någonting (som i exemplet ovan) eller ta bort vissa eller alla data från den nya applikationen. Justera det andra argumentet i RunPython
-operationen i enlighet med detta.
Ändra en ManyToManyField
så att den använder en through
-modell¶
Om du ändrar en ManyToManyField
till att använda en through
-modell kommer standardmigreringen att radera den befintliga tabellen och skapa en ny, vilket gör att de befintliga relationerna går förlorade. För att undvika detta kan du använda SeparateDatabaseAndState
för att byta namn på den befintliga tabellen till det nya tabellnamnet samtidigt som du talar om för migreringsautodetektorn att den nya modellen har skapats. Du kan kontrollera det befintliga tabellnamnet genom sqlmigrate
eller dbshell
. Du kan kontrollera det nya tabellnamnet med egenskapen _meta.db_table
i through-modellen. Din nya through
-modell bör använda samma namn för ForeignKey
som Django gjorde. Om den behöver några extra fält ska de också läggas till i operationer efter SeparateDatabaseAndState
.
Om vi till exempel hade en Book
-modell med ett ManyToManyField
som länkar till Author
, skulle vi kunna lägga till en genomgående modell AuthorBook
med ett nytt fält is_primary
, så här:
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),
),
]
Ändring av en ostyrd modell till styrd¶
Om du vill ändra en ohanterad modell (managed=False
) till hanterad måste du ta bort managed=False
och generera en migrering innan du gör andra schemarelaterade ändringar i modellen, eftersom schemaändringar som visas i den migrering som innehåller åtgärden för att ändra Meta.managed
kanske inte tillämpas.