Att hantera databasmigreringar är en stor utmaning i alla programvaruprojekt. Lyckligtvis kommer Django från och med version 1.7 med ett inbyggt migreringsramverk. Ramverket är mycket kraftfullt och användbart för att hantera förändringar i databaser. Men den flexibilitet som ramverket gav krävde vissa kompromisser. För att förstå begränsningarna med Django-migreringar ska du ta itu med ett välkänt problem:att skapa ett index i Django utan driftstopp.
I den här självstudien får du lära dig:
- Hur och när Django genererar nya migreringar
- Hur man inspekterar kommandona Django genererar för att utföra migrering
- Hur man säkert ändrar migreringarna så att de passar dina behov
Denna självstudie på mellannivå är designad för läsare som redan är bekanta med Django-migrering. För en introduktion till det ämnet, kolla in Django Migrations:A Primer.
Gratis bonus: Klicka här för att få gratis tillgång till ytterligare Django-handledningar och resurser som du kan använda för att fördjupa dina Python-webbutvecklingsfärdigheter.
Problemet med att skapa ett index i Django Migrations
En vanlig förändring som vanligtvis blir nödvändig när data som lagras av din applikation växer är att lägga till ett index. Index används för att snabba upp frågor och få din app att kännas snabb och lyhörd.
I de flesta databaser krävs ett exklusivt lås på bordet för att lägga till ett index. Ett exklusivt lås förhindrar datamodifieringsoperationer (DML) såsom UPDATE
, INSERT
och DELETE
, medan indexet skapas.
Lås erhålls implicit av databasen när vissa operationer utförs. Till exempel, när en användare loggar in på din app kommer Django att uppdatera last_login
fältet i auth_user
tabell. För att utföra uppdateringen måste databasen först få ett lås på raden. Om raden för närvarande låses av en annan anslutning kan du få ett databasundantag.
Att låsa en tabell kan utgöra ett problem när det är nödvändigt att hålla systemet tillgängligt under migrering. Ju större tabellen är, desto längre tid kan det ta att skapa indexet. Ju längre tid det tar att skapa indexet, desto längre tid är systemet otillgängligt eller svarar inte för användarna.
Vissa databasleverantörer tillhandahåller ett sätt att skapa ett index utan att låsa tabellen. Till exempel, för att skapa ett index i PostgreSQL utan att låsa en tabell, kan du använda SAMTIDIGT
nyckelord:
CREATE INDEX CONCURRENTLY ix ON table (column);
I Oracle finns en ONLINE
alternativ för att tillåta DML-operationer på tabellen medan indexet skapas:
CREATE INDEX ix ON table (column) ONLINE;
När jag genererar migrering kommer Django inte att använda dessa speciella nyckelord. Om migreringen körs som den är kommer databasen att få ett exklusivt lås på bordet och förhindra DML-operationer medan indexet skapas.
Att skapa ett index samtidigt har några varningar. Det är viktigt att förstå de problem som är specifika för din databasbackend i förväg. Till exempel, en varning i PostgreSQL är att det tar längre tid att skapa ett index samtidigt eftersom det kräver en extra tabellskanning.
I den här handledningen kommer du att använda Django-migreringar för att skapa ett index på en stor tabell, utan att orsaka stillestånd.
Obs! För att följa denna handledning rekommenderar vi att du använder en PostgreSQL-backend, Django 2.x och Python 3.
Det är möjligt att följa med andra databasbackends också. På platser där SQL-funktioner som är unika för PostgreSQL används, ändra SQL så att den matchar din databasbackend.
Inställningar
Du kommer att använda en påhittad Rea
modell i en app som heter app
. I en verklig situation, modeller som Rea
är huvudtabellerna i databasen, och de kommer vanligtvis att vara mycket stora och lagra mycket data:
# models.py
from django.db import models
class Sale(models.Model):
sold_at = models.DateTimeField(
auto_now_add=True,
)
charged_amount = models.PositiveIntegerField()
För att skapa tabellen, generera den första migreringen och tillämpa den:
$ python manage.py makemigrations
Migrations for 'app':
app/migrations/0001_initial.py
- Create model Sale
$ python manage migrate
Operations to perform:
Apply all migrations: app
Running migrations:
Applying app.0001_initial... OK
Efter ett tag blir försäljningsbordet väldigt stort, och användarna börjar klaga på långsamhet. När du övervakade databasen märkte du att många frågor använder sold_at
kolumn. För att snabba på saker och ting bestämmer du dig för att du behöver ett index på kolumnen.
För att lägga till ett index på sold_at
, gör du följande ändring av modellen:
# models.py
from django.db import models
class Sale(models.Model):
sold_at = models.DateTimeField(
auto_now_add=True,
db_index=True,
)
charged_amount = models.PositiveIntegerField()
Om du kör den här migreringen som den är, kommer Django att skapa indexet på bordet, och det kommer att låsas tills indexet är klart. Det kan ta ett tag att skapa ett index på ett mycket stort bord, och du vill undvika driftstopp.
I en lokal utvecklingsmiljö med en liten datauppsättning och väldigt få anslutningar kan denna migrering kännas omedelbar. På stora datamängder med många samtidiga anslutningar kan det dock ta ett tag att erhålla ett lås och skapa indexet.
I nästa steg kommer du att ändra migrering som skapats av Django för att skapa indexet utan att orsaka stillestånd.
Falsk migrering
Den första metoden är att skapa indexet manuellt. Du kommer att generera migreringen, men du kommer inte att faktiskt låta Django tillämpa den. Istället kommer du att köra SQL manuellt i databasen och sedan få Django att tro att migreringen är klar.
Generera först migreringen:
$ python manage.py makemigrations --name add_index_fake
Migrations for 'app':
app/migrations/0002_add_index_fake.py
- Alter field sold_at on sale
Använd sqlmigrate
kommandot för att se den SQL Django kommer att använda för att utföra denna migrering:
$ python manage.py sqlmigrate app 0002
BEGIN;
--
-- Alter field sold_at on sale
--
CREATE INDEX "app_sale_sold_at_b9438ae4" ON "app_sale" ("sold_at");
COMMIT;
Du vill skapa indexet utan att låsa tabellen, så du måste ändra kommandot. Lägg till SAMTIDIGT
nyckelord och kör i databasen:
app=# CREATE INDEX CONCURRENTLY "app_sale_sold_at_b9438ae4"
ON "app_sale" ("sold_at");
CREATE INDEX
Lägg märke till att du körde kommandot utan BEGIN
och COMMIT
delar. Om du utelämnar dessa nyckelord körs kommandona utan en databastransaktion. Vi kommer att diskutera databastransaktioner längre fram i artikeln.
Efter att du utfört kommandot, om du försöker tillämpa migrering, kommer du att få följande felmeddelande:
$ python manage.py migrate
Operations to perform:
Apply all migrations: app
Running migrations:
Applying app.0002_add_index_fake...Traceback (most recent call last):
File "venv/lib/python3.7/site-packages/django/db/backends/utils.py", line 85, in _execute
return self.cursor.execute(sql, params)
psycopg2.ProgrammingError: relation "app_sale_sold_at_b9438ae4" already exists
Django klagar på att indexet redan finns, så det kan inte fortsätta med migreringen. Du skapade precis indexet direkt i databasen, så nu måste du få Django att tro att migreringen redan har tillämpats.
Hur man fejkar en migrering
Django tillhandahåller ett inbyggt sätt att markera migreringar som exekverade, utan att faktiskt köra dem. För att använda det här alternativet, ställ in --fake
flagga när du tillämpar migreringen:
$ python manage.py migrate --fake
Operations to perform:
Apply all migrations: app
Running migrations:
Applying app.0002_add_index_fake... FAKED
Django gjorde inget fel den här gången. Faktum är att Django inte riktigt tillämpade någon migrering. Den markerade den precis som körd (eller FAKED
).
Här är några frågor att tänka på när du fejkar migrering:
-
Det manuella kommandot måste motsvara den SQL som genereras av Django: Du måste se till att kommandot du kör är likvärdigt med SQL som genereras av Django. Använd
sqlmigrate
för att producera SQL-kommandot. Om kommandona inte stämmer överens kan du få inkonsekvenser mellan databasen och modelltillståndet. -
Andra icke-tillämpade migreringar kommer också att förfalskas: När du har flera icke-tillämpade migrationer kommer alla att vara fejkade. Innan du tillämpar migrering är det viktigt att se till att endast de migreringar du vill fejka är otillämpade. Annars kan du få inkonsekvenser. Ett annat alternativ är att ange den exakta migreringen du vill fejka.
-
Direktåtkomst till databasen krävs: Du måste köra SQL-kommandot i databasen. Detta är inte alltid ett alternativ. Det är också farligt att utföra kommandon direkt i en produktionsdatabas och bör undvikas när det är möjligt.
-
Automatiska distributionsprocesser kan behöva justeras: Om du automatiserade distributionsprocessen (med hjälp av CI, CD eller andra automatiseringsverktyg) kan du behöva ändra processen till falska migrering. Detta är inte alltid önskvärt.
Rengöring
Innan du går vidare till nästa avsnitt måste du återställa databasen till dess tillstånd direkt efter den första migreringen. För att göra det, migrera tillbaka till den ursprungliga migreringen:
$ python manage.py migrate 0001
Operations to perform:
Target specific migration: 0001_initial, from app
Running migrations:
Rendering model states... DONE
Unapplying app.0002_add_index_fake... OK
Django avaktiverade ändringarna som gjordes i den andra migreringen, så nu är det säkert att även ta bort filen:
$ rm app/migrations/0002_add_index_fake.py
Inspektera migreringarna för att se till att du gjorde allt rätt:
$ python manage.py showmigrations app
app
[X] 0001_initial
Den första migreringen tillämpades, och det finns inga icke-tillämpade migreringar.
Kör rå SQL i migrering
I föregående avsnitt körde du SQL direkt i databasen och förfalskade migreringen. Detta gör jobbet gjort, men det finns en bättre lösning.
Django tillhandahåller ett sätt att exekvera rå SQL i migrering med RunSQL
. Låt oss försöka använda det istället för att köra kommandot direkt i databasen.
Skapa först en ny tom migrering:
$ python manage.py makemigrations app --empty --name add_index_runsql
Migrations for 'app':
app/migrations/0002_add_index_runsql.py
Redigera sedan migreringsfilen och lägg till en RunSQL
operation:
# migrations/0002_add_index_runsql.py
from django.db import migrations, models
class Migration(migrations.Migration):
atomic = False
dependencies = [
('app', '0001_initial'),
]
operations = [
migrations.RunSQL(
'CREATE INDEX "app_sale_sold_at_b9438ae4" '
'ON "app_sale" ("sold_at");',
),
]
När du kör migreringen får du följande utdata:
$ python manage.py migrate
Operations to perform:
Apply all migrations: app
Running migrations:
Applying app.0002_add_index_runsql... OK
Det här ser bra ut, men det finns ett problem. Låt oss försöka generera migrering igen:
$ python manage.py makemigrations --name leftover_migration
Migrations for 'app':
app/migrations/0003_leftover_migration.py
- Alter field sold_at on sale
Django genererade samma migrering igen. Varför gjorde den det?
Rengöring
Innan vi kan svara på den frågan måste du rensa upp och ångra ändringarna du gjort i databasen. Börja med att ta bort den senaste migreringen. Det tillämpades inte, så det är säkert att ta bort:
$ rm app/migrations/0003_leftover_migration.py
Lista sedan migreringarna för appen
app:
$ python manage.py showmigrations app
app
[X] 0001_initial
[X] 0002_add_index_runsql
Den tredje migreringen är borta, men den andra tillämpas. Du vill komma tillbaka till staten direkt efter den första migreringen. Försök att migrera tillbaka till den ursprungliga migreringen som du gjorde i föregående avsnitt:
$ python manage.py migrate app 0001
Operations to perform:
Target specific migration: 0001_initial, from app
Running migrations:
Rendering model states... DONE
Unapplying app.0002_add_index_runsql...Traceback (most recent call last):
NotImplementedError: You cannot reverse this operation
Django kan inte vända migreringen.
Omvänd migrering
För att vända en migrering utför Django en motsatt åtgärd för varje operation. I det här fallet är det omvända med att lägga till ett index att släppa det. Som du redan har sett, när en migrering är reversibel, kan du avaktivera den. Precis som du kan använda checkout
i Git kan du vända en migrering om du kör migrera
till en tidigare migrering.
Många inbyggda migreringsoperationer definierar redan en omvänd åtgärd. Till exempel är den omvända åtgärden för att lägga till ett fält att släppa motsvarande kolumn. Den omvända åtgärden för att skapa en modell är att ta bort motsvarande tabell.
Vissa migreringsoperationer är inte reversibla. Till exempel finns det ingen omvänd åtgärd för att ta bort ett fält eller ta bort en modell, eftersom när migreringen väl har tillämpats är data borta.
I föregående avsnitt använde du RunSQL
drift. När du försökte vända migreringen stötte du på ett fel. Enligt felet kan en av operationerna i migreringen inte ångras. Django kan inte vända rå SQL som standard. Eftersom Django inte har någon kunskap om vad som utfördes av operationen, kan den inte generera en motsatt handling automatiskt.
Hur man gör en migrering reversibel
För att en migrering ska vara reversibel måste alla operationer i den vara reversibla. Det är inte möjligt att vända en del av en migrering, så en enda icke-reversibel operation kommer att göra hela migreringen icke-reversibel.
För att göra en RunSQL
operation reversible, måste du tillhandahålla SQL för att exekvera när operationen är omvänd. Den omvända SQL-koden finns i reverse_sql
argument.
Motsatsen till att lägga till ett index är att släppa det. För att göra din migrering reversibel, tillhandahåll reverse_sql
för att ta bort indexet:
# migrations/0002_add_index_runsql.py
from django.db import migrations, models
class Migration(migrations.Migration):
atomic = False
dependencies = [
('app', '0001_initial'),
]
operations = [
migrations.RunSQL(
'CREATE INDEX "app_sale_sold_at_b9438ae4" '
'ON "app_sale" ("sold_at");',
reverse_sql='DROP INDEX "app_sale_sold_at_b9438ae4";',
),
]
Försök nu att vända migreringen:
$ python manage.py showmigrations app
app
[X] 0001_initial
[X] 0002_add_index_runsql
$ python manage.py migrate app 0001
Operations to perform:
Target specific migration: 0001_initial, from app
Running migrations:
Rendering model states... DONE
Unapplying app.0002_add_index_runsql... OK
$ python manage.py showmigrations app
app
[X] 0001_initial
[ ] 0002_add_index_runsql
Den andra migreringen vändes och indexet släpptes av Django. Nu är det säkert att ta bort migreringsfilen:
$ rm app/migrations/0002_add_index_runsql.py
Det är alltid en bra idé att tillhandahålla reverse_sql
. I situationer där reversering av en rå SQL-operation inte kräver någon åtgärd, kan du markera operationen som reversibel med hjälp av den speciella sentinel migrations.RunSQL.noop
:
migrations.RunSQL(
sql='...', # Your forward SQL here
reverse_sql=migrations.RunSQL.noop,
),
Förstå modelltillstånd och databastillstånd
I ditt tidigare försök att skapa indexet manuellt med RunSQL
, genererade Django samma migrering om och om igen trots att indexet skapades i databasen. För att förstå varför Django gjorde det måste du först förstå hur Django bestämmer när nya migreringar ska genereras.
När Django genererar en ny migrering
I processen att generera och tillämpa migrering synkroniserar Django mellan tillståndet för databasen och tillståndet för modellerna. Till exempel, när du lägger till ett fält i en modell, lägger Django till en kolumn i tabellen. När du tar bort ett fält från modellen tar Django bort kolumnen från tabellen.
För att synkronisera mellan modellerna och databasen upprätthåller Django ett tillstånd som representerar modellerna. För att synkronisera databasen med modellerna genererar Django migreringsoperationer. Migreringsoperationer översätts till en leverantörsspecifik SQL som kan köras i databasen. När alla migreringsoperationer är utförda förväntas databasen och modellerna vara konsekventa.
För att få reda på tillståndet för databasen, aggregerar Django operationerna från alla tidigare migreringar. När det aggregerade tillståndet för migreringarna inte överensstämmer med modellernas tillstånd, genererar Django en ny migrering.
I föregående exempel skapade du indexet med rå SQL. Django visste inte att du skapade indexet eftersom du inte använde en välbekant migreringsåtgärd.
När Django aggregerade alla migrationer och jämförde dem med modellernas tillstånd fann den att ett index saknades. Det är därför, även efter att du skapat indexet manuellt, trodde Django fortfarande att det saknades och genererade en ny migrering för det.
Hur man separerar databas och tillstånd vid migrering
Eftersom Django inte kan skapa indexet som du vill, vill du tillhandahålla din egen SQL men ändå låta Django veta att du skapade det.
Med andra ord måste du köra något i databasen och förse Django med migreringsoperationen för att synkronisera dess interna tillstånd. För att göra det ger Django oss en speciell migreringsoperation som heter SeparateDatabaseAndState
. Denna operation är inte välkänd och bör reserveras för speciella fall som detta.
Det är mycket lättare att redigera migrering än att skriva dem från början, så börja med att skapa en migrering på vanligt sätt:
$ python manage.py makemigrations --name add_index_separate_database_and_state
Migrations for 'app':
app/migrations/0002_add_index_separate_database_and_state.py
- Alter field sold_at on sale
Detta är innehållet i migreringen som genereras av Django, samma som tidigare:
# migrations/0002_add_index_separate_database_and_state.py
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('app', '0001_initial'),
]
operations = [
migrations.AlterField(
model_name='sale',
name='sold_at',
field=models.DateTimeField(
auto_now_add=True,
db_index=True,
),
),
]
Django genererade ett AlterField
operation i fältet sold_at
. Operationen kommer att skapa ett index och uppdatera tillståndet. Vi vill behålla den här operationen men tillhandahålla ett annat kommando att köra i databasen.
Återigen, för att få kommandot, använd SQL som genereras av Django:
$ python manage.py sqlmigrate app 0002
BEGIN;
--
-- Alter field sold_at on sale
--
CREATE INDEX "app_sale_sold_at_b9438ae4" ON "app_sale" ("sold_at");
COMMIT;
Lägg till SAMTIDIGT
sökord på lämplig plats:
CREATE INDEX CONCURRENTLY "app_sale_sold_at_b9438ae4"
ON "app_sale" ("sold_at");
Redigera sedan migreringsfilen och använd SeparateDatabaseAndState
för att tillhandahålla ditt modifierade SQL-kommando för exekvering:
# migrations/0002_add_index_separate_database_and_state.py
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('app', '0001_initial'),
]
operations = [
migrations.SeparateDatabaseAndState(
state_operations=[
migrations.AlterField(
model_name='sale',
name='sold_at',
field=models.DateTimeField(
auto_now_add=True,
db_index=True,
),
),
],
database_operations=[
migrations.RunSQL(sql="""
CREATE INDEX CONCURRENTLY "app_sale_sold_at_b9438ae4"
ON "app_sale" ("sold_at");
""", reverse_sql="""
DROP INDEX "app_sale_sold_at_b9438ae4";
"""),
],
),
],
Migreringsåtgärden SeparateDatabaseAndState
accepterar 2 listor med operationer:
- state_operationer är operationer att tillämpa på den interna modelltillståndet. De påverkar inte databasen.
- databasoperationer är operationer som ska tillämpas på databasen.
Du behöll den ursprungliga operationen som genererades av Django i state_operations
. När du använder SeparateDatabaseAndState
, det här är vad du vanligtvis vill göra. Lägg märke till att db_index=True
argument ges till fältet. Denna migreringsoperation kommer att meddela Django att det finns ett index på fältet.
Du använde SQL som genererades av Django och lade till RunSQL
för att köra rå SQL i migreringen.
Om du försöker köra migreringen får du följande utdata:
$ python manage.py migrate app
Operations to perform:
Apply all migrations: app
Running migrations:
Applying app.0002_add_index_separate_database_and_state...Traceback (most recent call last):
File "/venv/lib/python3.7/site-packages/django/db/backends/utils.py", line 83, in _execute
return self.cursor.execute(sql)
psycopg2.InternalError: CREATE INDEX CONCURRENTLY cannot run inside a transaction block
Icke-atomiska migrationer
I SQL, CREATE
, DROP
, ALTER
och TRUNCATE
operationer kallas Data Definition Language (DDL). I databaser som stöder transaktions-DDL, som PostgreSQL, kör Django som standard migrering i en databastransaktion. Men enligt felet ovan kan PostgreSQL inte skapa ett index samtidigt i ett transaktionsblock.
För att kunna skapa ett index samtidigt inom en migrering måste du be Django att inte utföra migreringen i en databastransaktion. För att göra det markerar du migreringen som icke-atomär genom att ställa in atomic
till False
:
# migrations/0002_add_index_separate_database_and_state.py
from django.db import migrations, models
class Migration(migrations.Migration):
atomic = False
dependencies = [
('app', '0001_initial'),
]
operations = [
migrations.SeparateDatabaseAndState(
state_operations=[
migrations.AlterField(
model_name='sale',
name='sold_at',
field=models.DateTimeField(
auto_now_add=True,
db_index=True,
),
),
],
database_operations=[
migrations.RunSQL(sql="""
CREATE INDEX CONCURRENTLY "app_sale_sold_at_b9438ae4"
ON "app_sale" ("sold_at");
""",
reverse_sql="""
DROP INDEX "app_sale_sold_at_b9438ae4";
"""),
],
),
],
När du har markerat migreringen som icke-atomär kan du köra migreringen:
$ python manage.py migrate app
Operations to perform:
Apply all migrations: app
Running migrations:
Applying app.0002_add_index_separate_database_and_state... OK
Du har precis kört migreringen utan att orsaka stillestånd.
Här är några problem att tänka på när du använder SeparateDatabaseAndState
:
-
Databasoperationer måste vara likvärdiga med tillståndsoperationer: Inkonsekvenser mellan databasen och modelltillståndet kan orsaka mycket problem. En bra utgångspunkt är att behålla operationerna som genereras av Django i
state_operations
och redigera utdata frånsqlmigrate
att använda idatabase_operations
. -
Icke-atomära migrationer kan inte återställas vid fel: Om det uppstår ett fel under migreringen kommer du inte att kunna återställa. Du måste antingen återställa migreringen eller slutföra den manuellt. Det är en bra idé att hålla de operationer som utförs inom en icke-atomär migration till ett minimum. Om du har ytterligare operationer i migreringen, flytta dem till en ny migrering.
-
Migreringen kan vara leverantörsspecifik: SQL som genereras av Django är specifik för den databasbackend som används i projektet. Det kan fungera med andra databasbackends, men det är inte garanterat. Om du behöver stödja flera databasbackends måste du göra några justeringar av detta tillvägagångssätt.
Slutsats
Du började den här handledningen med ett stort bord och ett problem. Du ville göra din app snabbare för dina användare och du ville göra det utan att orsaka dem några driftstopp.
I slutet av handledningen lyckades du generera och säkert modifiera en Django-migrering för att uppnå detta mål. Du tacklade olika problem längs vägen och lyckades övervinna dem med hjälp av inbyggda verktyg från migreringsramverket.
I den här självstudien lärde du dig följande:
- Hur Django-migreringar fungerar internt med modell- och databastillstånd, och när nya migreringar genereras
- Hur man kör anpassad SQL i migrering med
RunSQL
åtgärd - Vad är reversibla migreringar och hur man gör en
RunSQL
åtgärd reversibel - Vad är atommigrationer och hur man ändrar standardbeteendet efter dina behov
- Hur man säkert utför komplexa migreringar i Django
Separationen mellan modell och databastillstånd är ett viktigt begrepp. När du väl förstår det och hur du använder det kan du övervinna många begränsningar i de inbyggda migreringsoperationerna. Några användningsfall som kommer att tänka på inkluderar att lägga till ett index som redan skapats i databasen och tillhandahålla leverantörsspecifika argument till DDL-kommandon.