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.
La seule restriction est qu’avant PostgreSQL 11, l’ajout de colonnes avec des valeurs par défaut provoque une réécriture complète de la table, dans un temps proportionnel à sa taille. 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¶
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.
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 classesOperation
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 = '%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, 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? [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.
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 classeMigration
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 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
etdatetime.datetime
(y compris celles qui contiennent un fuseau horaire) - Instances
decimal.Decimal
- Instances
enum.Enum
- Instances
uuid.UUID
- instances de
functools.partial()
et defunctools.partialmethod
qui possèdent des valeurs sérialisables defunc
,args
etkeywords
. - Des objets chemins purs et concrets de
pathlib
. Les chemins concrets sont convertis en leur équivalent chemin pur, par ex.pathlib.PosixPath
enpathlib.PurePosixPath
. - Instances
os.PathLike
, par ex.os.DirEntry
, qui sont converties enstr
oubytes
en utilisantos.fspath()
. - instances
LazyObject
qui décorent une valeur sérialisable. - Instances de types énumératifs (ex.
TextChoices
ouIntegerChoices
). - 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)
La prise en charge de la sérialisation des objets chemins purs et concrets des instances pathlib
et os.PathLike
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.