Migrations

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.

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 et de l’annulation des migrations.

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

  • showmigrations, qui affiche la liste des migrations d’un projet ainsi que leur état.

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

Parmi ces bases de données, PostgreSQL est celle disposant du plus de capacités en terme de prise en charge de schéma.

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

MySQL 8.0 a introduit des améliorations de performance significatives pour les opérations de structures de données, en les rendant plus efficaces et en réduisant le besoin de reconstructions complètes de tables. Cependant, il ne peut pas garantir une absence complète de verrous ou d’interruptions. Dans les situations où les verrous sont encore nécessaires, la durée de ces opérations sera proportionnelle au nombre de lignes concernées.

Enfin, MySQL a une taille limite relativement petite 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

Django peut créer les migrations à votre place. 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':
  books/migrations/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:
  Apply all migrations: books
Running migrations:
  Rendering model states... DONE
  Applying books.0003_auto... OK

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.

Si vous souhaitez attribuer un nom éloquent à une migration au lieu d’un nom généré automatiquement, vous pouvez utilisez l’option makemigrations --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.

Transactions

Avec les bases de données qui gèrent les transactions pour les instructions de définition de schéma (SQLite et PostgreSQL), toutes les opérations de migration s’exécuteront par défaut dans une transaction unique. Au contraire, si une base de données ne gère pas ces transactions DDL (par ex. MySQL, Oracle), alors toutes les opérations seront exécutées sans transaction.

Il est possible d’empêcher qu’une migration s’exécute dans une transaction en définissant l’attribut atomic à False. Par exemple

from django.db import migrations


class Migration(migrations.Migration):
    atomic = False

Il est aussi possible d’exécuter certaines parties de la migration dans une transaction en utilisant atomic() ou en passant atomic=True à RunPython. Voir Migrations non atomiques pour plus de détails.

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

Les applications sans migrations ne doivent pas posséder de relations (ForeignKey`, ManyToManyField, etc.) vers de applications avec migrations. Cela peut parfois marcher, mais Django ne le garantit aucunement.

Dépendances interchangeables

django.db.migrations.swappable_dependency(value)

La fonction swappable_dependency() est utilisée dans les migrations pour déclarer les dépendances interchangeables sur les migrations de l’application du modèle inséré actuellement, dans la première migration de cette application. Par conséquent, le modèle inséré doit être créé lors de la migration initiale. L’argument value est une chaîne étiquette_app.modèle indiquant l’étiquette d’une application et le nom du modèle, par exemple "monapp.MonModele".

En utilisant swappable_dependency(), vous informez le système des migrations que la migration dépend d’une autre migration qui définit un modèle interchangeable, donnant la possibilité de substituer le modèle par une implémentation différente dans le futur. Ceci est typiquement utilisé pour faire référence à des modèles sujets à personnalisation ou remplacement, tels que le modèle d’utilisateur personnalisé (settings.AUTH_USER_MODEL, qui contient par défaut "auth.User") dans le système d’authentification de Django.

Fichiers de migrations

Les migrations sont stockées sous forme de fichiers sur disque nommés « fichiers de migrations ». Ces fichiers sont en réalité des fichiers Python normaux avec une structure d’objets convenue, rédigés 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

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.

Migrations initiales

Migration.initial

Les « migrations initiales » d’une application sont celles qui créent la première version des tables de l’application. Une application possède normalement une seule migration initiale, mais dans certains cas présentant des interdépendances de modèles complexes, elle peut en avoir deux ou même plus.

Les migrations initiales sont marquées avec un attribut initial = True sur la classe de migration. Si cet attribut est absent, une migration est tout de même considérée comme « initiale » s’il s’agit de la première migration de l’application (c’est-à-dire qu’elle ne présente aucune dépendance sur d’autres migrations de la même application).

Lorsque l’option migrate --fake-initial est utilisée, ces migrations initiales sont traitées spécialement. Pour une migration initiale qui crée une ou plusieurs tables (opérations CreateModel), Django vérifie que toutes ces tables existent déjà dans la base de données et considère alors la migration comme appliquée. De même, pour une migration qui ajoute un ou plusieurs champs (opérations AddField), Django vérifie que toutes les colonnes concernées existent déjà dans la base de données et considère la migration comme appliquée le cas échéant. Sans --fake-initial, les migrations initiales sont traitées exactement comme toutes les autres migrations.

Cohérence de l’historique

Comme discuté précédemment, il peut être nécessaire de réconcilier manuellement des migrations lorsque deux branches de développement sont fusionnées. Lors de l’édition des dépendances de migration, il arrive qu’on crée involontairement un état historique incohérent où une migration a été appliquée mais pas certaines de ses dépendances. C’est une indication claire que les dépendances ne sont pas correctes, Django va donc refuser d’exécuter les migrations en d’en créer de nouvelles tant que ce n’est pas corrigé. Lors de l’utilisation de plusieurs bases de données, il est possible de faire appel à la méthode allow_migrate() des routeurs de base de données pour indiquer pour quelles bases de données makemigrations va contrôler l’historique.

Ajout de migrations aux applications

Les nouvelles applications sont préconfigurées pour accepter les migrations, il est possible d’ajouter des migrations en exécutant 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 en exécutant :

$ 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 l’option migrate --fake-initial, la commande 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.

Inversion des migrations

Les migrations peuvent être inversées avec migrate en passant le numéro de la migration précédente. Par exemple, pour inverser la migration books.0003:

$ python manage.py migrate books 0002
Operations to perform:
  Target specific migration: 0002_auto, from books
Running migrations:
  Rendering model states... DONE
  Unapplying books.0003_auto... OK
...\> py manage.py migrate books 0002
Operations to perform:
  Target specific migration: 0002_auto, from books
Running migrations:
  Rendering model states... DONE
  Unapplying books.0003_auto... OK

Si vous souhaitez défaire toutes les migrations appliquées d’une application, utilisez le terme zero:

$ python manage.py migrate books zero
Operations to perform:
  Unapply all migrations: books
Running migrations:
  Rendering model states... DONE
  Unapplying books.0002_auto... OK
  Unapplying books.0001_initial... OK
...\> py manage.py migrate books zero
Operations to perform:
  Unapply all migrations: books
Running migrations:
  Rendering model states... DONE
  Unapplying books.0002_auto... OK
  Unapplying books.0001_initial... OK

Une migration est irréversible si elle contient au moins une opération irréversible. Si on tente d’inverser une telle migration, une exception IrreversibleError sera générée :

$ python manage.py migrate books 0002
Operations to perform:
  Target specific migration: 0002_auto, from books
Running migrations:
  Rendering model states... DONE
  Unapplying books.0003_auto...Traceback (most recent call last):
django.db.migrations.exceptions.IrreversibleError: Operation <RunSQL  sql='DROP TABLE demo_books'> in books.0003_auto is not reversible
...\> py manage.py migrate books 0002
Operations to perform:
  Target specific migration: 0002_auto, from books
Running migrations:
  Rendering model states... DONE
  Unapplying books.0003_auto...Traceback (most recent call last):
django.db.migrations.exceptions.IrreversibleError: Operation <RunSQL  sql='DROP TABLE demo_books'> in books.0003_auto is not reversible

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 devez utiliser ces versions historisées de modèles et non pas les importer directement.

Avertissement

Si vous importez des modèles directement plutôt que d’utiliser les modèles historisés, vos migrations peuvent fonctionner initialement, mais échoueront dans le futur lorsque vous essaierez de relancer des anciennes migrations (ce qui arrive habituellement lors d’une nouvelle installation qui rejoue toutes les migrations pour créer la base de données).

Cela signifie que les problèmes de modèles historisés peuvent ne pas apparaître immédiatement. Si vous rencontrez ce genre de problème, il est correct de modifier la migration pour utiliser les modèles historisés au lieu des importations directes et de valider ces modifications.

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 concrètes des modèles sont 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.

Pour supprimer d’anciennes références, vous pouvez fusionner les migrations ou, s’il n’y a pas trop de références, les copier dans les fichiers de migration.

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

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 ou trois versions principales 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 :

# Generated by Django A.B on YYYY-MM-DD HH:MM
from django.db import 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 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 :

from django.db import 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 = f"{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, nous pouvons 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. 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, sauf si elles sont marquées comme elidable).

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 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. Transmettez-lui 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? [y/N] y
Optimizing...
  Optimized from 12 operations to 7 operations.
Created new squashed migration /home/andrew/Programs/DjangoTest/test/migrations/0001_squashed_0004_undo_something.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.

Utilisez l’option squashmigrations --squashed-name si vous souhaitez définir le nom de la migration fusionnée plutôt que d’utiliser le nom généré automatiquement.

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.

Vous devez alors faire passer la migration fusionnée vers une migration normale en :

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

  • mettant à jour toutes les migrations qui dépendent des migrations supprimées pour les faire dépendre de la migration fusionnée ;

  • enlevant l’attribut 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.

Nettoyage des références aux migrations supprimées

S’il est probable que vous puissiez réutiliser le nom d’une migration supprimée dans le futur, vous devriez supprimer les références à cette migration dans la table des migrations de Django avec l’option migrate --prune.

Sérialisation de valeurs

Les migrations sont des 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, float, bool, str, bytes, None, NoneType

  • list, set, tuple, dict, range.

  • instances datetime.date, datetime.time et datetime.datetime (y compris celles qui contiennent un fuseau horaire)

  • instances decimal.Decimal

  • instances enum.Enum et enum.Flag

  • instances uuid.UUID

  • instances de functools.partial() et de functools.partialmethod qui possèdent des valeurs sérialisables de func, args et keywords

  • des objets chemins purs et concrets de pathlib. Les chemins concrets sont convertis en leur équivalent chemin pur, par ex. pathlib.PosixPath en pathlib.PurePosixPath

  • instances os.PathLike, par ex. os.DirEntry, qui sont converties en str ou bytes en utilisant os.fspath()

  • instances LazyObject qui décorent une valeur sérialisable

  • instances de types énumératifs (ex. TextChoices ou IntegerChoices)

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

  • méthodes non liées utilisées depuis l’intérieur du corps de la classe

  • 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 5.0:

La prise en charge de la sérialisation des fonctions décorées avec functools.cache() ou functools.lru_cache() a été ajoutée.

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

Sérialiseurs personnalisés

Vous pouvez sérialiser d’autres types en écrivant un sérialiseur personnalisé. Par exempl, si Django ne savait pas sérialiser Decimal par défaut, vous auriez pu écrire

from decimal import Decimal

from django.db.migrations.serializer import BaseSerializer
from django.db.migrations.writer import MigrationWriter


class DecimalSerializer(BaseSerializer):
    def serialize(self):
        return repr(self.value), {"from decimal import Decimal"}


MigrationWriter.register_serializer(Decimal, DecimalSerializer)

Le premier paramètre de MigrationWriter.register_serializer() est un type ou un itérable de types qui doivent utiliser ce sérialiseur.

La méthode serialize() de votre sérialiseur doit renvoyer une chaîne contenant la représentation de la valeur dans les migrations et un ensemble de toute importation nécessaire dans la migration.

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

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.

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