Écriture de migrations de base de données

Ce document présente la façon de structurer et d’écrire des migrations de base de données pour différents scénarios auxquels vous pourriez être confrontés. Pour du contenu introductif aux migrations, consultez le guide thématique.

Migrations de données et bases de données multiples

Lorsque vous utilisez plusieurs bases de données, il peut être nécessaire de déterminer si une migration doit être appliquée à une certaine base de données. Par exemple, il est possible qu’une migration ne doive s’appliquer qu’à une seule base de données.

Pour faire cela, il est possible de se baser sur l’alias de base de données de la connexion dans une opération RunPython en consultant l’attribut schema_editor.connection.alias:

from django.db import migrations

def forwards(apps, schema_editor):
    if not schema_editor.connection.alias == 'default':
        return
    # Your migration code goes here

class Migration(migrations.Migration):

    dependencies = [
        # Dependencies to other migrations
    ]

    operations = [
        migrations.RunPython(forwards),
    ]
New in Django 1.8.

Vous pouvez également fournir des indications qui seront transmises à la méthode allow_migrate() des routeurs de base de données, dans **hints:

myapp/dbrouters.py
class MyRouter(object):

    def allow_migrate(self, db, app_label, model_name=None, **hints):
        if 'target_db' in hints:
            return db == hints['target_db']
        return True

Puis, pour s’appuyer sur ce code dans une migration, procédez comme suit :

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

Si votre opération RunPython ou RunSQL n’affecte qu’un seul modèle, il est de bon aloi de passer model_name comme indicateur pour la rendre aussi transparente que possible par rapport au routeur. C’est particulièrement important pour les applications tierces et réutilisables.

Migrations ajoutant des champs uniques

L’application d’une « simple » migration qui ajoute un champ unique non nul à une table qui contient déjà des lignes va produire une erreur car la valeur utilisée pour remplir les champs existants n’est générée qu’une seule fois, ce qui va à l’encontre de la contrainte d’unicité.

Il est donc nécessaire de suivre les étapes suivantes. Dans cet exemple, nous allons ajouter un champ non nul UUIDField avec une valeur par défaut. Modifiez le champ adéquat en fonction de vos propres besoins.

  • Ajoutez le champ à votre modèle avec les paramètres default=uuid.uuid4 et unique=True (choisissez une valeur par défaut adéquate au type de champ que vous ajoutez).

  • Lancez la commande makemigrations. Cela devrait générer une migration contenant une opération AddField.

  • Générez deux fichiers de migration vides pour la même application en exécutant deux fois makemigrations monapp --empty. Nous avons renommé les fichiers de migration dans les exemples ci-dessous pour leur donner des noms plus explicites.

  • Copiez l’opération AddField à partir de la migration auto-générée (le premier des trois nouveaux fichiers) vers la dernière migration et modifiez AddField en AlterField. Par exemple :

    0006_remove_uuid_null.py
    # -*- coding: utf-8 -*-
    # Generated by Django A.B on YYYY-MM-DD HH:MM
    from __future__ import unicode_literals
    
    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),
            ),
        ]
    
  • Modifiez le premier fichier de migration. La classe de migration générée devrait ressembler à ceci :

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

    Modifiez unique=True en null=True, ce qui créera le champ intermédiaire pouvant contenir la valeur null et va différer la création de la contrainte d’unicité en attendant que nous ayons terminé de remplir les valeurs uniques pour chaque ligne.

  • Dans le premier fichier de mgiration vide, ajoutez une opération RunPython ou RunSQL pour générer une valeur unique (UUID dans l’exemple) pour chaque ligne existante. Par exemple :

    0005_populate_uuid_values.py
    # -*- coding: utf-8 -*-
    # Generated by Django A.B on YYYY-MM-DD HH:MM
    from __future__ import unicode_literals
    
    from django.db import migrations, models
    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()
    
    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),
        ]
    
  • Vous pouvez maintenant appliquer les migrations comme d’habitude avec la commande migrate.

    Notez qu’il existe un conflit de concurrence si vous permettez de créer des objets lorsque cette migration se déroule. Les objets créés après l’opération AddField mais avant RunPython verront leur valeur originale uuid écrasée.

Contrôle de l’ordre des migrations

Django détermine l’ordre dans lequel les migrations doivent être appliquées non pas selon le nom de fichier des migrations, mais en construisant un graphe basé sur deux propriétés de la classe Migration: dependencies et run_before.

Si vous avez déjà utilisé la commande makemigrations, vous avez probablement vu la présence de dependencies car les migrations créées automatiquement définissent cet attribut dans le cadre du processus de création.

La propriété dependencies est déclarée comme ceci :

from django.db import migrations

class Migration(migrations.Migration):

    dependencies = [
        ('myapp', '0123_the_previous_migration'),
    ]

Cela suffit en général, mais de temps à autre, il peut être nécessaire de s’assurer que la migration s’exécute avant une autre. C’est par exemple utile pour que des migrations d’applications tierces s’exécutent après celles du modèle de remplacement AUTH_USER_MODEL.

Pour faire cela, placez toutes les migrations qui doivent dépendre des vôtres dans l’attribut run_before de votre classe Migration:

class Migration(migrations.Migration):
    ...

    run_before = [
        ('third_party_app', '0001_do_awesome'),
    ]

Privilégiez autant que possible l’usage de dependencies par rapport à run_before. run_before ne devrait être utilisé que dans le cas où il n’est pas souhaitable ou pas possible de définir dependencies dans la migration que vous voulez faire exécuter après celle que vous êtes en train d’écrire.

Migrating data between third-party apps

You can use a data migration to move data from one third-party application to another.

If you plan to remove the old app later, you’ll need to set the dependencies property based on whether or not the old app is installed. Otherwise, you’ll have missing dependencies once you uninstall the old app. Similarly, you’ll need to catch LookupError in the apps.get_model() call that retrieves models from the old app. This approach allows you to deploy your project anywhere without first installing and then uninstalling the old app.

Here’s a sample migration:

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

Also consider what you want to happen when the migration is unapplied. You could either do nothing (as in the example above) or remove some or all of the data from the new application. Adjust the second argument of the RunPython operation accordingly.

Back to Top