Come creare migrazioni di database¶
Questo documento spiega come strutturare e scrivere migrazioni di database per diversi scenari che potresti incontrare. Per materiale introduttivo sulle migrazioni, vedere the topic guide.
Migrazioni dati e database multipli¶
Quando usi più database, potresti aver bisogno di capire se è necessario utilizzare una migrazione per un particolare database. Per esempio, puoi voler lanciare solo una migrazione verso uno specifico database.
Per fare ciò è possibile controllare l’alias della connessione del database all’interno di un’operazione RunPython
guardando l’attributo 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),
]
Puoi anche fornire suggerimenti che verranno passati al metodo allow_migrate()
del router di database come **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
Quindi, per sfruttare questo nelle tue migrazioni, procedi come segue:
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'}),
]
Se la tua operazione RunPython
o RunSQL
interessa solo un modello, è buona norma passare model_name
come suggerimento per renderlo il più trasparente possibile al router. Ciò è particolarmente importante per le app riutilizzabili e di terze parti.
Migrazioni che aggiungono campi univoci¶
L’applicazione di una migrazione «plain» che aggiunge un campo univoco non annullabile a una tabella con righe esistenti genererà un errore poiché il valore utilizzato per popolare le righe esistenti viene generato una sola volta, interrompendo così il vincolo univoco.
Pertanto, è necessario eseguire i seguenti passaggi. In questo esempio, aggiungeremo un non-nullable UUIDField
con un valore predefinito. Modifica il rispettivo campo in base alle tue esigenze.
Aggiungi il campo al tuo modello con gli argomenti
default=uuid.uuid4
eunique=True
(scegli un valore predefinito appropriato per il tipo di campo che stai aggiungendo).Esegui il comando
makemigrations
. Questo dovrebbe generare una migrazione con un’operazioneAddField
.Genera due file di migrazione vuoti per la stessa app eseguendo due volte
makemigrations myapp --empty
. Abbiamo rinominato i file di migrazione per assegnare loro nomi significativi negli esempi seguenti.Copia l’operazione
AddField
dalla migrazione generata automaticamente (il primo dei tre nuovi file) all’ultima migrazione, cambiaAddField
inAlterField
e aggiungi le importazioni diuuid
emodels
. Per esempio:# 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), ), ]
Modifica il primo file di migrazione. La classe di migrazione generata sarà simile a questa:
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), ), ]
Cambia
unique=True
innull=True
– questo creerà il campo null intermedio e rinvierà la creazione del vincolo univoco fino a quando non avremo popolato valori univoci su tutte le righe.Nel primo file di migrazione vuoto, aggiungi una operazione
RunPython
oRunSQL
per generare un valore univoco (UUID nell’esempio) per ogni riga esistente. Aggiungi anche un import diuuid
. Per esempio:# 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), ]
Ora puoi applicare le migrazioni come al solito con il comando
migrate
.Nota che si crea una race condition se permetti agli oggetti di essere creati mentre la migrazione gira. Gli oggetti creati dopo
AddField
e prima diRunPython
avranno il lorouuid
originale sovrascritto.
Migrazioni «Non-atomic»¶
In un database che supporta le transazioni DDL (SQLite e PostgreSQL), le migrations sono eseguite di default all’interno di una transazione. Per casi d’uso quali migrations su grandi tabelle, potresti voler evitare l’esecuzione delle migrations all’interno di una transazione, configurando l’attributo atomic
a False
:
from django.db import migrations
class Migration(migrations.Migration):
atomic = False
In tale migrazione, tutte le operazioni sono lanciate senza transazione. E” possibile eseguire parti della migrazione dentro una transazione usando atomic()
o passando atomic=True
a RunPython
.
Qui c’è un esempio di migrazione dei dati non atomica che aggiorna una grande tabella con piccoli passi automatici:
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),
]
L’attributo atomic
non ha effetto sui database che non supportano le transazioni DDL (per es. MySQL, Oracle). (Il supporto per le istruzioni atomic DDL di MySQL si riferisce a singole istruzioni, piuttosto che a più istruzioni racchiuse in una transazione sulla quale si può fare roll back.)
Controllo dell’ordine delle migrazioni¶
Django determina l’ordine in cui le migrazioni dovrebbero essere applicate non in base al nome del file di ogni migrazione, ma usando un grafo che usa due proprietà sulla classe Migration
: dependencies
e run_before
.
Se hai usato il comando makemigrations
probabilmente hai già visto dependencies
in azione perchè le migrazioni auto-generate lo hanno definito come parte del loro processo di creazione.
La proprietà dependencies
viene dichiarata in questo modo:
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('myapp', '0123_the_previous_migration'),
]
Generalmente questo basta, ma di volta in volta potresti aver bisogno di assicurarti che le tue migrazioni girino prima di altre migrazioni. Questo torna utile, ad esempio, per fare in modo che le migrazioni delle app di terze parti girino dopo i rimpiazzi sul tuo AUTH_USER_MODEL
.
Per ottenere ciò, inserisci tutte le migrazioni che dovrebbero dipendere dalla tua nell’attributo run_before
nella tua classe Migration
:
class Migration(migrations.Migration):
...
run_before = [
('third_party_app', '0001_do_awesome'),
]
È preferibile usare dipendenze
su run_before
quando possibile. Dovresti usare run_before
solo se non è desiderabile o non pratico specificare dependencies
nella migrazione che vuoi eseguire dopo quella che stai scrivendo.
Migrazione dei dati tra app¶
È possibile utilizzare una migrazione dei dati per spostare i dati da un’applicazione ad un’altra.
Se prevedi di rimuovere la vecchia app in un secondo momento, dovrai impostare la proprietà dependencies
in base al fatto che la vecchia app sia installata o meno. Altrimenti, avrai le dipendenze mancanti dopo aver disinstallato la vecchia app. Allo stesso modo, dovrai catturare LookupError
nella chiamata apps.get_model()
che recupera i modelli dalla vecchia app. Questo approccio ti consente di distribuire il tuo progetto ovunque senza prima installare e quindi disinstallare la vecchia app.
Ecco un esempio di migrazione:
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'))
Potresti non fare nulla (come nell’esempio sopra) o rimuovere alcuni o tutti i dati dalla nuova applicazione. Regola di conseguenza il secondo argomento dell’operazione RunPython
.
Modifica di un ManyToManyField
per utilizzare un modello through
¶
Se modifichi una ManyToManyField
per usare un modello through
, la migrazione predefinita cancellerà la tabella esistente e ne creerà una nuova, perdendo le relazioni esistenti. Per evitare ciò, puoi utilizzare SeparateDatabaseAndState
per rinominare la tabella esistente con il nuovo nome della tabella, comunicando al rilevatore automatico di migrazione che il nuovo modello è stato creato. Puoi controllare il nome della tabella esistente tramite sqlmigrate
o dbshell
. Puoi controllare il nuovo nome della tabella attraverso la proprietà _meta.db_table
del modello. Il tuo nuovo modello through
dovrebbe usare gli stessi nomi per ForeignKey
s di Django. Inoltre, se sono necessari campi aggiuntivi, dovrebbero essere aggiunti nelle operazioni dopo SeparateDatabaseAndState
.
Per esempio, se avessimo un modello Book
con un ManyToManyField
collegato a Author
, potremmo aggiungere un modello completo AuthorBook
con un nuovo campo is_primary
, come:
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),
),
]
Modifica di un modello non gestito in gestito¶
Se desideri modificare un modello non gestito (managed=False
) in gestito, devi rimuovere managed=False
e generare una migrazione prima di creare un altro schema -modifiche correlate al modello, poiché le modifiche allo schema che appaiono nella migrazione che contiene l’operazione di modifica di Meta.managed
potrebbero non essere applicate.