• en
  • Langue : fr

Migrations

New in Django 1.7.

Les migrations sont la manière par laquelle Django propage des modifications que vous apportez à des modèles (ajout d’un champ, suppression d’un modèle, etc.) dans un schéma de base de données. Elles sont conçues pour être quasiment automatiques, mais vous aurez besoin de savoir quand créer les migrations, quand les exécuter, et les problèmes courants que vous pourriez rencontrer.

Un historique rapide

Avant la version 1.7, Django ne savait que gérer l’ajout de nouveaux modèles à la base de données ; il n’était pas possible de modifier ou supprimer des modèles existants via la commande syncdb (le prédécesseur de migrate).

Des outils externes, notamment South, permettaient ces autres types de modifications, mais la fonctionnalité a été jugé suffisamment importante pour qu’elle soit intégrée au noyau Django.

Les commandes

Il y a plusieurs commandes utiles pour interagir avec les migrations et manipuler le schéma de base de données avec Django :

  • migrate, qui est responsable de l’exécution des migrations, de leur annulation ainsi que de la gestion de leur état.

  • makemigrations, qui est responsable de la création de nouvelles migrations en fonction des modifications que vous avez apportées aux modèles.

  • sqlmigrate, qui affiche les instructions SQL correspondant à une migration.

Il est intéressant de noter que les migrations sont créées et gérées par application. En particulier, il est possible d’avoir des applications qui n’utilisent pas les migrations (celles-ci sont considérées comme des applications « non migrées ») ; ces applications utiliseront le comportement précédent de Django, qui consiste simplement à ajouter les nouveaux modèles.

Vous devez imaginer les migrations comme un système de contrôle de versions pour un schéma de base de données. makemigrations se charge de regrouper vos changements de modèle dans des fichiers de migration individuels - comme des commits - et migrate est chargé d’appliquer les changements à la base de données.

Les fichiers de migration pour chaque application sont stockés dans un répertoire « migrations » à l’intérieur de l’application, et sont conçus pour faire partie du code source et de la distribution de cette application. Les migrations sont créées une fois sur votre machine de développement, puis exécutées telles quelles sur les machines de vos collègues, les machines de tests, et finalement sur les machines de production.

Note

Pour chaque application, il est possible de redéfinir le nom du paquet qui contient les migrations en utilisant le réglage MIGRATION_MODULES.

Les migrations se déroulent toujours de la même manière lorsqu’elles sont appliquées sur un même ensemble de données et leurs résultats sont constants et répétables, ce qui signifie que ce que vous voyez dans le développement et les machines de test correspondra exactement à ce qui va se passer en production si les conditions d’exécution sont identiques.

Django crée des migrations pour tout changement dans les modèles ou les champs, même pour des options qui n’affectent pas la base de données, car pour lui la seule manière de reconstruire un champ correctement est d’avoir accès à l’historique de toutes les modifications ; vous pourriez avoir besoin de ces options lors de migrations de données ultérieures (par exemple, si vous avez défini des règles de validation particulières).

Bases de données prises en charge

Les migrations sont prises en charge sur tous les moteurs de bases de données livrées avec Django, ainsi que les bases de données tierces si elles prennent en charge les modifications de schéma (effectuées par l’intermédiaire de la classe SchemaEditor).

Toutefois, certaines bases de données ont plus de fonctionnalités que d’autres quand il s’agit de migrations de schéma ; un certain nombre de mises en garde sont rapportées ci-dessous.

PostgreSQL

PostgreSQL est la base de données dans Django qui a le plus de fonctionnalités concernant les schémas ; le seul inconvénient est que l’ajout de colonnes avec des valeurs par défaut entraîne une réécriture complète de la table, avec un temps d’exécution proportionnel à la taille de la table.

Pour cette raison, il est recommandé de toujours créer de nouvelles colonnes avec null=True, car de cette façon elles seront ajoutées immédiatement.

MySQL

MySQL ne crée pas de transactions pour les opérations de modification de schéma, ce qui signifie que si une migration ne parvient pas à s’appliquer, vous devrez défaire manuellement les modifications pour essayer de nouveau (il est impossible de revenir à un point de sauvegarde antérieur).

En outre, MySQL réécrit entièrement les tables pour presque toutes les opérations de schéma et a généralement besoin d’un temps d’exécution proportionnel au nombre de lignes des tables pour ajouter ou supprimer des colonnes. Sur une machine un peu lente, cela peut prendre plus d’une minute par million de lignes ; l’ajout de quelques colonnes à une table de quelques millions d’entrées pourrait bloquer votre site pendant plus de dix minutes.

Enfin, MySQL a des tailles limites relativement petites pour la longueur des noms de colonnes, tables et index, ainsi qu’une limite sur la taille combinée de toutes les colonnes qu’un index recouvre. Cela signifie que des index qui sont possibles sur d’autres bases de données ne pourront pas toujours être créés avec MySQL.

SQLite

SQLite ne gère nativement que très peu d’opérations de modifications de schéma, Django tente donc de les émuler par :

  • La création d’une nouvelle table pour le nouveau schéma

  • La copie des données de l’une à l’autre

  • La suppression de l’ancienne table

  • Le renommage de la nouvelle table avec le nom de l’ancienne table

Ce processus fonctionne généralement bien, mais il peut être lent et comporter des anomalies. Il n’est pas recommandé d’utiliser et de faire migrer SQLite dans un environnement de production, sauf si vous êtes pleinement conscient des risques et des limites ; la prise en charge de Django de cette fonctionnalité est conçue pour permettre aux développeurs d’utiliser SQLite sur leurs machines en local pour développer des projets Django moins complexes sans avoir besoin d’une base de données complète.

Procédures

L’utilisation des migrations est simple. Modifiez vos modèles - par exemple en ajoutant un champ ou en supprimant un modèle - puis exécutez makemigrations:

$ python manage.py makemigrations
Migrations for 'books':
  0003_auto.py:
    - Alter field author on book

Les modèles sont analysés et comparés aux versions actuellement contenues dans les fichiers de migration. Puis, un nouveau jeu de migrations est rédigé. Il est chaudement conseillé de lire le résultat produit pour voir ce que makemigrations a détecté comme changements. Cette commande n’est pas parfaite, et pour des changements complexes, il se peut qu’elle n’ait pas détecté ce que vous attendiez.

Lorsque les nouveaux fichiers de migration sont créés, on peut les appliquer à la base de données pour s’assurer qu’ils fonctionnent correctement :

$ python manage.py migrate
Operations to perform:
  Synchronize unmigrated apps: sessions, admin, messages, auth, staticfiles, contenttypes
  Apply all migrations: books
Synchronizing apps without migrations:
  Creating tables...
  Installing custom SQL...
  Installing indexes...
Installed 0 object(s) from 0 fixture(s)
Running migrations:
  Applying books.0003_auto... OK

La commande s’exécute en deux parties. Premièrement, elle synchronise les applications sans migrations (effectuant le même processus que faisait anciennement syncdb), puis elle exécute toute migration qui n’a pas encore été appliquée.

Quand la migration a été appliquée, ajoutez la migration et les modifications des modèles dans votre système de gestion de versions par un seul « commit » ; de cette façon, lorsque d’autres développeurs (ou votre serveur de production) obtiennent le nouveau code, ils reçoivent en même temps les modifications des modèles et la migration qui leur correspond.

New in Django 1.8.

Si vous souhaitez attribuer un nom éloquent à une migration au lieu d’un nom généré automatiquement, vous pouvez utilisez l’option --name:

$ python manage.py makemigrations --name changed_my_model your_app_label

Contrôle de versions

Comme les migrations sont stockées dans le système de gestion de versions, il peut arriver de temps à autre qu’un autre développeur enregistre une migration pour la même application en même temps que vous, ce qui aboutit à deux migrations préfixées du même numéro.

Soyez sans crainte, les numéros servent uniquement comme référence pour les développeurs, Django exige seulement que chaque migration soit nommée différemment. Les migrations définissent dans leur fichier les autres migrations dont elles dépendent, y compris les migrations précédentes de la même application, il est donc possible de détecter lorsqu’il y a deux nouvelles migrations pour la même application sans préférence d’ordre.

Lorsque ceci se produit, Django vous pose la question avec plusieurs options. S’il pense que c’est sans risque, il offre de fusionner automatiquement les deux migrations pour vous. Dans le cas contraire, il vous faudra modifier vous-même les migrations, mais ce n’est pas une opération difficile et elle est expliquée plus en détails dans Fichiers de migrations ci-dessous.

Dépendances

Bien que les migrations sont spécifiques à chaque application, les tables et les relations déterminées par les modèles sont trop complexes pour être seulement créées pour une application à la fois. Lorsque vous créez une migration qui nécessite qu’une autre opération soit exécutée, par exemple l’ajout d’une clé ForeignKey de l’application livres vers l’application auteurs, la migration résultante contiendra une dépendance sur une migration dans auteurs.

Cela signifie que quand vous exécutez les migrations, la migration de auteurs s’exécute en premier et crée la table référencée par la clé étrangère, puis la migration qui crée la colonne ForeignKey peut s’exécuter et créer la contrainte. Si cela ne se faisait pas ainsi, la migration essaierait de créer une colonne ForeignKey sans que la table référencée n’existe et la base de données signalerait une erreur.

Ce comportement de dépendances affecte la majorité des opérations de migration qui sont limitées à une seule application. La restriction à une seule application (que ce soit pour makemigrations ou pour migrate) est respectée dans la mesure du possible, mais ce n’est pas une garantie ; toute autre application concernée dans l’optique de la gestion des dépendances sera également impliquée dans l’opération.

Il faut cependant savoir que les applications sans migrations ne peuvent pas dépendre d’applications avec migrations, par la nature même de l’absence de migrations. Cela signifie qu’il n’est généralement pas possible d’avoir une application sans migrations qui possède une clé ForeignKey ou un champ ManyToManyField vers une application avec migrations ; cela peut marcher dans certains cas, mais risque bien de finir par des erreurs.

Avertissement

Même si les choses semblent fonctionner avec des applications sans migrations dépendant d’applications avec migrations, il se peut que Django ne produise pas toutes les contraintes de clé étrangère nécessaires !

C’est particulièrement visible quand on utilise des modèles interchangeables (par ex. AUTH_USER_MODEL), car chaque application qui utilise ces modèles aura besoin de migrations si vous n’avez pas de chance. Le temps passant, de plus en plus d’applications tierces posséderont des migrations, mais dans l’intervalle, vous pouvez soit leur ajouter vous-même des migrations (en utilisant MIGRATION_MODULES pour stocker ces modules en dehors des modules propres à l’application, si vous le voulez), soit laisser l’application contenant votre modèle utilisateur sans migrations.

De plus, tout modèle utilisé dans des opérations RunPython doit posséder des migrations afin que leurs relations à d’autres modèles puissent être créées convenablement.

Fichiers de migrations

Les migrations sont stockées sous forme de fichiers sur disque nommés « fichiers de migrations ». Ce ne sont en réalité que des fichiers Python normaux avec une structure d’objets convenue, rédigée dans un style déclaratif.

Un fichier de migration simple ressemble à ceci :

from django.db import migrations, models

class Migration(migrations.Migration):

    dependencies = [("migrations", "0001_initial")]

    operations = [
        migrations.DeleteModel("Tribble"),
        migrations.AddField("Author", "rating", models.IntegerField(default=0)),
    ]

Lorsque Django charge un fichier de migration (sous forme de module Python), Django recherche une sous-classe de django.db.migrations.Migration nommée Migration. Il inspecte ensuite cet objet en cherchant quatre attributs, parmi lesquels deux sont utilisés la plupart du temps :

  • dependencies, une liste de migrations dont celle-ci dépend.

  • operations, une liste de classes Operation définissant ce que fait cette migration.

L’élément central, c’est les opérations ; il s’agit d’un ensemble d’instructions déclaratives indiquant à Django quelles sont les modifications de schéma qu’il doit effectuer. Django les analyse et construit une représentation en mémoire de tous ces changements de schéma pour toutes les applications, puis utilise cela pour générer le code SQL qui procédera aux modifications de schéma.

Cette structure en mémoire est également utilisée pour calculer les différences entre les modèles et l’état actuel des migrations ; Django parcourt toutes les modifications dans l’ordre en les appliquant à un ensemble de modèles en mémoire afin d’aboutir à l’état des modèles à l’instant de la dernière exécution de makemigrations. Il utilise ensuite ces modèles pour les comparer à ceux qui se trouvent dans les fichiers models.py afin de déterminer ce qui a changé.

Il est rarement nécessaire de devoir modifier des fichiers de migration à la main, mais il est tout à fait possible de les écrire manuellement en cas de besoin. Certaines des opérations les plus complexes ne sont pas détectables automatiquement et ne sont donc disponibles que si on les écrit manuellement ; il ne faut donc pas avoir peur de modifier les migrations en cas de nécessité.

Champs personnalisés

Il n’est pas possible de modifier le nombre de paramètres positionnels dans un champ personnalisé déjà migré sans générer une exception TypeError. L’ancienne migration appellera la méthode __init__ modifiée avec l’ancienne signature. Si donc vous avez besoin d’un nouveau paramètre, créez plutôt un paramètre nommé et ajoutez quelque chose comme assert 'nom_du_paramètre' in kwargs dans le constructeur.

Gestionnaires de modèles

New in Django 1.8.

Si vous le souhaitez, il est possible de sérialiser les gestionnaires d’objets dans les migrations pour qu’ils soient disponibles lors des opérations RunPython. Cela peut se faire en définissant un attribut use_in_migrations sur la classe du gestionnaire :

class MyManager(models.Manager):
    use_in_migrations = True

class MyModel(models.Model):
    objects = MyManager()

Si vous utilisez la fonction from_queryset() pour générer dynamiquement une classe de gestionnaire, il est nécessaire d’hériter de la classe générée pour la rendre importable :

class MyManager(MyBaseManager.from_queryset(CustomQuerySet)):
    use_in_migrations = True

class MyModel(models.Model):
    objects = MyManager()

Veuillez vous référer aux notes sur les Modèles historiques dans les migrations pour connaître les implications que cela génère.

Ajout de migrations aux applications

L’ajout de migrations à de nouvelles applications est très simple, car elles sont préconfigurées pour accepter les migrations ; il suffit d’exécuter makemigrations après avoir effectué un certain nombre de modifications.

Si l’application possède déjà des modèles et des tables de base de données et qu’elle n’a pas encore de migrations (par exemple parce que vous l’avez créée avec une version précédente de Django), vous allez devoir la convertir en vue de l’utilisation des migrations. Il s’agit d’un procédé simple :

$ python manage.py makemigrations your_app_label

Cela va créer une nouvelle migration initiale pour l’application. Ensuite, lancez python manage.py migrate --fake-initial, et Django détectera qu’une migration initiale est présente et que les tables qu’il doit créer existent déjà ; il va alors marquer la migration comme déjà appliquée (sans le drapeau --fake-initial, la commande migrate produirait une erreur car les tables qu’elle essayerait de créer existent déjà).

Notez que cela ne fonctionne qu’à deux conditions :

  • Les modèles n’ont pas été modifiés depuis la création des tables correspondantes. Pour que les migrations fonctionnent, vous devez d’abord créer la migration initiale, puis faire les modifications, car Django compare les modifications aux fichiers de migration, pas à la base de données.

  • La base de données n’a pas été modifiée manuellement. Django n’est pas capable de détecter que la base de données ne correspond pas aux modèles, et au moment où les migrations essaieront de modifier ces tables, vous obtiendrez des erreurs.

Changed in Django 1.8:

L’option --fake-initial a été ajoutée à migrate. Précédemment, les migrations initiales étaient toujours « pseudo-appliquées » automatiquement si des tables existantes étaient détectées.

Modèles historiques

Lorsque vous exécutez des migrations, Django se base sur des versions historiques des modèles stockées dans les fichiers de migration. Si vous rédigez du code Python en utilisant l’opération RunPython ou si vous avez des méthodes allow_migrate sur vos routeurs de base de données, vous serez confronté à ces versions de vos modèles.

Comme il n’est pas possible de sérialiser du code Python arbitraire, ces modèles historiques ne posséderont pas de méthodes personnalisées que vous auriez pu définir. Ils auront cependant les mêmes champs, relations, gestionnaires (pour ceux qui possèdent l’attribut use_in_migrations = True) et options Meta (également historiques, pouvant donc être différents des éléments actuels).

Avertissement

Cela signifie que des méthodes save() personnalisées ne seront PAS appelées pour les objets manipulés dans les migrations, de la même manière que des constructeurs ou des méthodes d’instance personnalisées ne sont PAS accessibles. Planifiez avec précaution !

Les références à des fonctions dans les options de champ telles que upload_to, limit_choices_to et les déclarations de gestionnaire de modèle ayant l’attribut use_in_migrations = True sont sérialisées dans les migrations, ce qui fait que ces fonctions et classes doivent être conservées quelque part dans le code aussi longtemps que des migrations les référencent. Il s’agit également de conserver les champs de modèle personnalisés, car ils sont importés directement par les migrations.

De plus, les classes de base des modèles sont simplement stockées sous forme de pointeurs, il faut donc toujours conserver ces classes de base quelque part aussi longtemps qu’une migration contient une référence vers elles. Le côté positif est que les méthodes et gestionnaires de ces classes de base héritent de façon normale, ce qui fait que si vous avez absolument besoin d’y avoir accès, une possibilité est de les déplacer dans une classe parente.

Considérations lors de la suppression de champs de modèles

New in Django 1.8.

Dans le même ordre d’idée que les considérations sur les « références aux fonctions historiques » dans la section précédente, la suppression de champs de modèles personnalisés d’un projet ou d’une application tierce posera un problème si ces champs sont référencés dans d’anciennes migrations.

Pour vous assister dans cette situation, Django fournit quelques attributs de champs de modèle qui peuvent aider à rendre obsolètes des champs de modèle à l’aide de l’ infrastructure des contrôles systèmes.

Ajoutez l’attribut system_check_deprecated_details au champ de modèle concerné de cette manière :

class IPAddressField(Field):
    system_check_deprecated_details = {
        'msg': (
            'IPAddressField has been deprecated. Support for it (except '
            'in historical migrations) will be removed in Django 1.9.'
        ),
        'hint': 'Use GenericIPAddressField instead.',  # optional
        'id': 'fields.W900',  # pick a unique ID for your field.
    }

Après une période d’obsolescence de votre choix (deux versions majeures pour les champs propres à Django), modifiez l’attribut system_check_deprecated_details en system_check_removed_details et mettez à jour le dictionnaire selon le modèle suivant :

class IPAddressField(Field):
    system_check_removed_details = {
        'msg': (
            'IPAddressField has been removed except for support in '
            'historical migrations.'
        ),
        'hint': 'Use GenericIPAddressField instead.',
        'id': 'fields.E900',  # pick a unique ID for your field.
    }

Il faut toujours conserver les méthodes de champ qui lui sont nécessaires pour fonctionner dans le contexte de migrations de base de données, telles que __init__(), deconstruct() et get_internal_type(). Conservez ce champ historisé aussi longtemps que des migrations le référençant existent encore. Par exemple, après avoir fusionné des migrations et effacé les anciennes, il est alors possible de supprimer complètement cet ancien champ.

Migrations de données

En plus de modifier le schéma de base de données, les migrations peuvent aussi être utilisées pour modifier les données mêmes de la base de données, conjointement avec le schéma, si vous le souhaitez.

Les migrations qui modifient des données sont généralement appelées des « migrations de données » ; il est préférable d’en faire des migrations séparées, en parallèle des migrations de schéma.

Django ne sait pas générer automatiquement des migrations de données à votre place, comme il le fait pour les migrations de schéma, mais il n’est pas très compliqué de les écrire. Les fichiers de migration dans Django sont composés d’Operations, et la principale opération utilisée pour les migrations de données est RunPython.

Pour commencer, créez un fichier de migration vide qui constituera votre point de départ (Django place le fichier au bon endroit, suggère un nom et ajoute les dépendances pour vous) :

python manage.py makemigrations --empty yourappname

Puis, ouvrez le fichier ; il devrait ressembler à quelque chose comme ceci :

# -*- coding: utf-8 -*-
from django.db import models, migrations

class Migration(migrations.Migration):

    dependencies = [
        ('yourappname', '0001_initial'),
    ]

    operations = [
    ]

Tout ce qu’il vous reste à faire est de créer une nouvelle fonction et de demander à RunPython de l’appeler. RunPython s’attend à un objet exécutable en paramètre qui accepte lui-même deux paramètres : le premier est un registre d’applications contenant les versions historisées de tous les modèles qui y sont chargés afin de correspondre à l’endroit où se situe la migration dans l’historique, et le second est une classe SchemaEditor permettant d’effectuer manuellement des modifications de schéma de la base de données (mais prenez garde, de telles modifications pourraient embrouiller l’auto-détecteur de migrations !).

Écrivons une migration simple qui remplit notre nouveau champ name avec les valeurs combinées de first_name et last_name (nous sommes retombés sur nos pieds et avons réalisé que tout le monde n’a pas forcément un nom et un prénom). Tout ce que nous avons à faire est d’utiliser le modèle historique et de parcourir chaque ligne :

# -*- coding: utf-8 -*-
from django.db import models, migrations

def combine_names(apps, schema_editor):
    # We can't import the Person model directly as it may be a newer
    # version than this migration expects. We use the historical version.
    Person = apps.get_model("yourappname", "Person")
    for person in Person.objects.all():
        person.name = "%s %s" % (person.first_name, person.last_name)
        person.save()

class Migration(migrations.Migration):

    dependencies = [
        ('yourappname', '0001_initial'),
    ]

    operations = [
        migrations.RunPython(combine_names),
    ]

Une fois que c’est fait, il suffit de lancer normalement python manage.py migrate et la migration de données sera exécutée comme toute autre migration.

Il est possible de passer un second objet exécutable à RunPython pour exécuter toute logique adéquate dans le cas d’une migration inverse. Si cet objet est omis, la migration inverse générera une exception, le cas échéant.

Accèder aux modèles d’autres applications

Lorsque vous écrivez une fonction appelée par RunPython et qui utilise des modèles issus d’applications autres que celle dans laquelle la migration est située, l’attribut dependencies de la migration doit inclure la dernière migration de chaque application qui est en cause, sinon vous pouvez obtenir une erreur semblable à : LookupError: Aucune application installée pour 'myappname' lorsque vous utilisez apps.get_model() pour essayer de récupérer le modèle dans la fonction appelée par RunPython.

Dans l’exemple suivant, nous avons une migration dans app1 qui a besoin d’utiliser des modèles dans app2. Nous ne sommes pas préoccupés par les détails de move_m1 mis à part que cette fonction aura besoin d’accéder à des modèles des deux applications. Par conséquent, nous avons ajouté une dépendance qui spécifie la dernière migration de app2

class Migration(migrations.Migration):

    dependencies = [
        ('app1', '0001_initial'),
        # added dependency to enable using models from app2 in move_m1
        ('app2', '0004_foobar'),
    ]

    operations = [
        migrations.RunPython(move_m1),
    ]

Migrations avancées

Si vous êtes intéressé aux opérations de migration plus avancées ou que vous voulez pouvoir écrire votre propre opération, consultez la référence des opérations de migration et le guide pratique sur l’écriture de migrations.

Fusion de migrations

Il ne faut pas craindre de créer autant de migrations que nécessaire, ce n’est pas un problème. Le code de migration est optimisé pour traiter des centaines de migrations à la fois sans trop de lenteurs. Cependant, il peut arriver un moment où l’on souhaite réduire un grand nombre de migrations à juste quelques-unes, et c’est là que la fusion des migrations intervient.

La fusion est l’acte de réduire un ensemble existant de nombreuses migrations à une seule (ou parfois quelques-unes) qui représente toujours les mêmes changements.

Django opère cela en prenant toutes les migrations existantes, extrayant leurs opérations en les plaçant toutes à la suite, puis exécutant un optimiseur pour essayer de réduire au maximum cette liste d’opérations. Par exemple, il sait que CreateModel et DeleteModel s’annulent l’une l’autre et que AddField peut être intégrée dans CreateModel.

Une fois que la suite d’opérations a été réduite au minimum, Django écrit le résultat dans une nouvel ensemble de fichiers de migration initiale. Ce nombre minimum dépend de la quantité de dépendances relatives entre les modèles et de la présence d’opérations RunSQL ou RunPython (qui ne peuvent pas subir d’optimisation).

Dans ces fichiers, il est indiqué qu’ils remplacent les migrations fusionnées précédentes, ce qui signifie qu’ils peuvent coexister avec les anciens fichiers de migration ; Django choisit intelligemment les fichiers à prendre en compte en fonction de la position actuelle dans l’historique de migration. Si un projet n’a pas encore complètement appliqué un ensemble de migrations qui a été fusionné, Django continue d’utiliser ces anciennes migrations jusqu’au moment de la fusion, après quoi il se rattache à l’historique fusionné, tandis que de nouvelles installations du projet vont directement utiliser la nouvelle migration fusionnée et laisser de côté les anciennes.

Cela permet de fusionner des migrations sans perturber des systèmes actuellement en production qui ne sont pas encore totalement à jour. Le processus recommandé est de fusionner, de conserver les anciens fichiers, de valider et publier le résultat, d’attendre jusqu’à ce que tous les systèmes soient mis à jour avec la nouvelle version (ou dans le cas d’une application réutilisable, s’assurer que les utilisateurs mettent à jour en suivant les versions sans en sauter), puis de supprimer les anciens fichiers, de valider et de publier une seconde version.

La commande qui se charge de tout ceci est squashmigrations. Il suffit de lui transmettre l’étiquette d’application et le nom de la migration jusqu’à laquelle vous souhaitez fusionner, et elle se mettra au travail :

$ ./manage.py squashmigrations myapp 0004
Will squash the following migrations:
 - 0001_initial
 - 0002_some_change
 - 0003_another_change
 - 0004_undo_something
Do you wish to proceed? [yN] y
Optimizing...
  Optimized from 12 operations to 7 operations.
Created new squashed migration /home/andrew/Programs/DjangoTest/test/migrations/0001_squashed_0004_undo_somthing.py
  You should commit this migration but leave the old ones in place;
  the new migration will be used for new installs. Once you are sure
  all instances of the codebase have applied the migrations you squashed,
  you can delete them.

Sachez que les interdépendances de modèles Django peuvent devenir vraiment complexes et la fusion peut aboutir à des migrations qui ne peuvent pas être exécutées ; il peut soit y avoir un problème d’optimisation (auquel cas vous pouvez essayer une nouvelle fois avec l’option --no-optimize, mais il serait aussi judicieux de signaler le problème), soit un problème d’erreur CircularDependencyError, et dans ce cas, vous pouvez le résoudre manuellement.

Pour résoudre manuellement une erreur CircularDependencyError, séparez l’une des clés étrangères de la boucle de dépendances circulaires dans une migration distincte, puis déplacez la dépendance sur l’autre application qui la contient. Si vous hésitez, examinez comment makemigrations s’occupe de ce problème lorsqu’on lui demande de créer de toutes nouvelles migrations à partir de vos modèles. Dans une version future de Django, squashmigrations sera mise à jour pour qu’elle puisse résoudre ces erreurs par elle-même.

Après avoir fusionné les migrations, ajoutez la migration résultante en parallèle à celles qu’elle remplace et distribuez cette modification à toutes les instances en production de votre projet, en prenant soin d’exécuter chaque fois migrate pour enregistrer la modification dans la base de données.

Quand cela a été fait, vous devez alors faire passer la migration fusionnée vers une migration initiale normale en :

  • supprimant tous les fichiers de migration qu’elle remplace ;

  • enlevant le paramètre replaces dans la classe Migration de la migration fusionnée (c’est ce qui permet à Django de savoir qu’il s’agit d’une migration fusionnée).

Note

Après avoir créé une migration fusionnée, vous ne pouvez pas refusionner celle-ci avant d’avoir effectué la transition complète vers une migration normale.

Sérialisation de valeurs

Les migrations sont de simples fichiers Python contenant les anciennes définitions de vos modèles. Pour pouvoir les écrire, Django doit donc prendre l’état actuel des modèles et les sérialiser dans un fichier.

Bien que Django puisse sérialiser la plupart des objets, il y en a certains qui ne peuvent simplement pas être sérialisés en une représentation Python valide. Il n’existe pas de standard Python permettant à une valeur d’être retransformée en code (repr() ne fonctionne qu’avec des valeurs basiques et ne définit pas de chemins d’importation).

Django est capable de sérialiser ce qui suit :

  • int, long, float, bool, str, unicode, bytes, None
  • list, set, tuple, dict
  • Instances datetime.date, datetime.time et datetime.datetime (y compris celles qui contiennent un fuseau horaire)

  • Instances decimal.Decimal

  • Tous les champs Django

  • Toute référence de fonction ou de méthode (par ex. datetime.datetime.today) (doit être définie au niveau principal du module)

  • Toute référence de classe (doit être définie au niveau principal du module)

  • Tout objet comportant une méthode deconstruct() (voir ci-dessous)

Changed in Django 1.7.1:

La prise en charge de la sérialisation des objets datetime avec fuseau horaire a été ajoutée.

Les éléments suivants ne peuvent être sérialisés par Django qu’en Python 3 :

  • Méthodes non liées utilisées depuis l’intérieur du corps de la classe (voir ci-dessous)

Django ne peut pas sérialiser :

  • Les classes imbriquées

  • Des instances de classe arbitraires (par ex. MaClasse(4.3, 5.7))

  • Les fonctions lambdas

Comme __qualname__ n’a été introduit que dans Python 3, Django ne peut sérialiser l’exemple ci-dessous (une méthode non liée utilisée depuis l’intérieur du corps de la classe) qu’avec Python 3 ; en Python 2, la sérialisation de cette référence échouera :

class MyModel(models.Model):

    def upload_to(self):
        return "something dynamic"

    my_file = models.FileField(upload_to=upload_to)

Si vous utilisez Python 2, il est recommandé de placer les méthodes pour upload_to et d’autres paramètres semblables qui acceptent des objets exécutables (par ex. default) dans le corps du module principal, plutôt que dans le corps d’une classe.

Ajout d’une méthode deconstruct()

Vous pouvez permettre à Django de sérialiser vos propres instances de classe personnalisées en définissant une méthode deconstruct() pour la classe. Elle n’accepte pas de paramètres et doit renvoyer un tuple de trois éléments (path, args, kwargs):

  • path doit contenir le chemin Python vers la classe, avec le nom de la classe inclus en dernier (par exemple, monapp.quelque_chose.MaClasse). Si la classe n’est pas définie au premier niveau du module, elle ne peut pas être sérialisée.

  • args doit être une liste de paramètres positionnels à passer à la méthode __init__ de la classe. Tous les éléments de cette liste doivent être eux-mêmes sérialisables.

  • kwargs doit être un dictionnaire de paramètres nommés à passer à la méthode __init__ de la classe. Toutes ses valeurs doivent être elles-mêmes sérialisables.

Note

Cette valeur de renvoi est différente de la méthode deconstruct() des champs personnalisés qui renvoie un tuple de quatre éléments.

Django écrit la valeur sous forme d’instanciation de la classe avec les paramètres donnés, de la même façon qu’il écrit les références aux champs Django.

Pour éviter qu’une nouvelle migration soit créée lors de chaque exécution de makemigrations, vous devriez aussi ajouter une méthode __eq__() à la classe décorée. Cette fonction sera appelée par le système des migrations de Django pour détecter d’éventuels changements d’état.

Pour autant que tous les paramètres passés au constructeur de la classe sont eux-même sérialisables, vous pouvez utiliser le décorateur de classe @deconstructible se trouvant dans django.utils.deconstruct pour ajouter la méthode deconstruct():

from django.utils.deconstruct import deconstructible

@deconstructible
class MyCustomClass(object):

    def __init__(self, foo=1):
        self.foo = foo
        ...

    def __eq__(self, other):
        return self.foo == other.foo

Ce décorateur ajoute la logique nécessaire pour capturer et préserver les paramètres lors de leur transmission au constructeur, puis renvoie ces mêmes paramètres lorsque deconstruct() est appelée.

Prise en charge de Python 2 et 3

Dans l’optique de générer des migrations qui prennent en charge à la fois Python 2 et 3, toutes les chaînes de texte littéral dans vos modèles et champs (par ex. verbose_name, related_name, etc.) doivent être des chaînes qui restent toujours soit des chaînes d’octets, soit des chaînes de texte (unicode), que ce soit avec Python 2 ou 3 (au lieu de représenter des octets avec Python 2 et du texte avec Python 3, ce qui correspond à la situation par défaut pour les chaînes littérales non marquées spécialement). Sinon, l’exécution de makemigrations avec Python 3 risque de produire de nouvelles migrations bizarres qui convertissent tous ces attributs textuels en texte.

La manière la plus simple de faire cela est de suivre les conseils donnés dans le guide de migration vers Python 3 et de s’assurer que tous vos modules commencent par from __future__ import unicode_literals afin que toutes les chaînes littérales soient toujours de l’unicode, quelle que soit la version de Python. Lorsque vous ajoutez cela à une application possédant déjà des migrations générées avec Python 2, le prochain lancement de makemigrations avec Python 3 va probablement générer de multiples changements dans la mesure où tous les attributs de type chaîne d’octets sont convertis en chaînes de texte ; c’est normal et ne devrait se produire qu’une seule fois.

Prendre en charge plusieurs versions de Django

Si vous êtes responsable d’une application tierce qui contient des modèles, vous devrez peut-être fournir des migrations qui prennent en charge plusieurs versions de Django. Dans ce cas, vous devez toujours exécuter makemigrations avec le plus bas niveau de version de Django vous souhaitez prendre en charge.

Le système de migrations maintiendra la compatibilité ascendante selon la même politique que le reste de Django, afin que les fichiers de migration générées sur Django X.Y devraient fonctionner sans modification sur Django X.Y+1. Cependant, le système des migrations ne promet pas de compatibilité descendante. De nouvelles fonctionnalités peuvent être ajoutées, et les fichiers de migration générés avec les nouvelles versions de Django pourront ne pas fonctionner sur les anciennes versions.

Mise à jour à partir de South

Si vous avez déjà des migrations existantes créées avec South, le processus de mise à niveau vers l’utilisation de django.db.migrations est relativement simple :

  • Assurez-vous que toutes les installations sont totalement à jour avec les migrations actuelles.

  • Enlevez 'south' du réglage INSTALLED_APPS.

  • Supprimez tous les fichiers de migration (numérotés), sauf le répertoire des migrations et le fichier __init__.py. Prenez soin de supprimer également les fichiers .pyc.

  • Exécutez python manage.py makemigrations. Django devrait détecter les répertoires de migrations vides et créer de nouvelles migrations initiales dans le nouveau format.

  • Exécuter python manage.py migrate --fake-initial. Django verra que les tables pour les migrations initiales existent déjà et les marquera comme appliquées sans les exécuter (Django ne contrôle pas que les schémas des tables correspondent à vos modèles, mais simplement que les noms de table existent).

Et c’est tout ! La seule complication peut provenir de dépendances circulaires de clés étrangères ; dans ce cas, makemigrations pourrait produire plus d’une migration initiale et vous devrez donc ensuite toutes les marquer comme appliquées en utilisant :

python manage.py migrate --fake yourappnamehere
Changed in Django 1.8:

L’option --fake-initial a été ajoutée à migrate; précédemment, les migrations initiales étaient toujours « pseudo-appliquées » automatiquement si des tables existantes étaient détectées.

Bibliothèques / applications tierces

Si vous êtes le mainteneur d’une bibliothèque ou d’une application tierce, et que vous souhaitez prendre en charge à la fois les migrations South (pour Django 1.6 et précédentes versions) et les migrations Django (à partir de Django 1.7), il est possible de conserver deux jeux de migrations parallèles dans l’application, chacune dans son format.

Pour contribuer à cela, South 1.0 va automatiquement et premièrement rechercher des migrations au format South dans un répertoire south_migrations avant de regarder dans migrations, ce qui signifie que les projets des utilisateurs utiliseront les bonnes migrations de manière transparente, pour autant que vous placiez les migrations South dans un répertoire nommé south_migrations et les migrations Django dans le répertoire migrations.

Vous trouverez de plus amples informations dans les notes de publication de South 1.0.

Voir aussi

La référence des opérations de migration

Documente l’API des opérations de schéma, les opérations spéciales ainsi que l’écriture de ses propres opérations.

Le guide pratique sur l’écriture de migrations

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.

Back to Top