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:

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

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 e unique=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’operazione AddField.

  • 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, cambia AddField in AlterField e aggiungi le importazioni di uuid e models. Per esempio:

    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),
            ),
        ]
    
  • Modifica il primo file di migrazione. La classe di migrazione generata sarà simile a questa:

    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),
            ),
        ]
    

    Cambia unique=True in null=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 o RunSQL per generare un valore univoco (UUID nell’esempio) per ogni riga esistente. Aggiungi anche un import di uuid. Per esempio:

    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),
        ]
    
  • 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 di RunPython avranno il loro uuid 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:

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'))

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 ForeignKeys 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.

Back to Top