Opérations de migration

Les fichiers de migration sont composés de un ou plusieurs objets Operation qui enregistrent de manière déclarative les opérations à appliquer à la base de données.

Django utilise également ces objets Operation pour recomposer un état historisé de vos modèles et pour calculer les modifications effectuées aux modèles depuis la dernière migration afin de pouvoir écrire automatiquement les migrations ; c’est pourquoi ils sont déclaratifs, ce qui permet à Django de facilement tous les charger en mémoire et de les parcourir sans devoir accéder à la base de données pour trouver à quoi le projet devrait ressembler.

Certains objets Operation plus spécialisés sont destinés à des opérations comme les migrations de données et pour des manipulations manuelles avancées de la base de données. Vous pouvez même écrire vos propres classes Operation si vous le voulez pour englober des modifications que vous effectuez fréquemment.

Si vous avez besoin d’un fichier de migration vide pour y écrire vos propres objets Operation, exécutez python manage.py makemigrations --empty nom_application, mais soyez conscient que l’ajout manuel d’opérations modifiant le schéma peut perturber l’autodétecteur de migrations et provoquer du code incorrect durant les prochaines exécutions de makemigrations.

Toutes les opérations Django de base se trouvent dans le module django.db.migrations.operations.

Pour du contenu d’initiation, consultez le guide thématique des migrations.

Opérations liées au schéma

CreateModel

class CreateModel(name, fields, options=None, bases=None, managers=None)

Crée un nouveau modèle dans l’historique du projet et une table correspondante dans la base de données.

name est le nom du modèle tel qu’il est écrit dans le fichier models.py.

fields est une liste de tuples binaires (nom_champ, instance_champ). L’instance de champ doit être un champ non lié (« unbound ») (donc simplement models.CharField(…), plutôt qu’un champ repris d’un autre modèle).

options est un dictionnaire facultatif de valeurs tirées de la classe Meta du modèle.

bases est une liste facultative d’autres classes dont ce modèle doit hériter ; elle peut contenir aussi bien des objets classes que des chaînes au format "nomapp.NomModele" si vous voulez dépendre d’un autre modèle (vous héritez donc de sa version historique). En cas d’absence, la valeur par défaut hérite du modèle standard models.Model.

managers accepte une liste de tuples binaires (nom_gestionnaire, instance_gestionnaire). Le premier gestionnaire de la liste sera le gestionnaire par daéfut de ce modèle pour les migrations.

DeleteModel

class DeleteModel(name)

Supprime le modèle de l’historique du projet et ses tables de la base de données.

RenameModel

class RenameModel(old_name, new_name)

Renomme le modèle d’un ancien nom vers un nouveau nom.

Il peut arriver que vous deviez ajoutez manuellement cette opération si vous modifiez à la fois le nom du modèle et plusieurs de ses champs ; pour l’autodétecteur, il paraîtra que l’ancien modèle a été supprimé et qu’un nouveau modèle a été ajouté avec un nom différent, ce qui fera que la migration créée perdra toutes les données de l’ancienne table.

AlterModelTable

class AlterModelTable(name, table)

Modifie le nom de la table du modèle (l’option db_table de la classe interne Meta).

AlterUniqueTogether

class AlterUniqueTogether(name, unique_together)

Modifie l’ensemble de contraintes d’unicité du modèle (l’option unique_together de la classe interne Meta).

AlterIndexTogether

class AlterIndexTogether(name, index_together)

Modifie l’ensemble d’index personnalisés du modèle (l’option index_together de la classe interne Meta).

AlterOrderWithRespectTo

class AlterOrderWithRespectTo(name, order_with_respect_to)

Crée ou supprime la colonne _order nécessaire à l’option order_with_respect_to de la classe interne Meta.

AlterModelOptions

class AlterModelOptions(name, options)

Stocke les modifications de diverses options de modèles (attributs de la classe Meta d’un modèle) comme permissions ou verbose_name. N’affecte pas la base de données, mais fait persister ces modifications à l’usage des instances RunPython. options doit être un dictionnaire faisant correspondre des noms d’options à leurs valeurs.

AlterModelManagers

class AlterModelManagers(name, managers)

Modifie les gestionnaires disponibles durant les migrations.

AddField

class AddField(model_name, name, field, preserve_default=True)

Ajoute un champ à un modèle. model_name est le nom du modèle, name est le nom du champ et field est une instance de champ Field non liée (ce que vous placeriez dans une déclaration de champ dans models.py, par exemple models.IntegerField(null=True)).

Le paramètre preserve_default indique si la valeur par défaut du champ est permanente et doit être incluse dans l’état du projet (True) ou si elle n’est que temporaire et juste pour la migration (False), généralement parce que la migration ajoute un champ non nul à une table et qu’elle a besoin d’une valeur par défaut pour remplir les lignes existantes. Cela ne concerne pas le comportement de définition de valeurs par défaut au sein même de la base de données, car Django ne définit jamais de valeurs par défaut pour la base de données et les applique toujours au niveau du code Django de l’ORM.

Avertissement

Sur les bases de données plus anciennes, l’ajout d’un champ avec valeur par défaut peut provoquer une réécriture complète de la table. Cela arrive même pour des champs avec valeur nulle autorisée et peut affecter les performances en baisse. Pour éviter cela, il s’agit de suivre les étapes suivantes.

  • Ajoutez le champ avec valeur nulle autorisée sans valeur par défaut et lancez la commande makemigrations. Cela devrait générer une migration contenant une opération AddField.
  • Ajoutez la valeur par défaut à votre champ et lancez la commande makemigrations. Cela devrait générer une migration contenant une opération AlterField.

RemoveField

class RemoveField(model_name, name)

Supprime un champ d’un modèle.

Gardez en tête que lors de l’inversion de l’opération, un nouveau champ est ajouté au modèle. L’opération est réversible (sans prendre en compte la perte de données qui est irréversible) à la condition que le champ puisse valoir null ou qu’il possède une valeur par défaut utilisable pour remplir la colonne recréée. Dans le cas contraire, l’opération est irréversible.

AlterField

class AlterField(model_name, name, field, preserve_default=True)

Modifie la définition d’un champ, y compris les modifications de son type, des attributs null, unique, db_column ou des autres attributs de champ.

Le paramètre preserve_default indique si la valeur par défaut du champ est permanente et doit être incluse dans l’état du projet (True) ou si elle n’est que temporaire et juste pour la migration (False), généralement parce que la migration modifie un champ nul vers un champ non nul à une table et qu’elle a besoin d’une valeur par défaut pour remplir les lignes existantes. Cela ne concerne pas le comportement de définition de valeurs par défaut au sein même de la base de données, car Django ne définit jamais de valeurs par défaut pour la base de données et les applique toujours au niveau du code Django de l’ORM.

Notez qu’il n’est pas toujours possible d’effectuer toutes les modifications, aussi en fonction de la base de données. Par exemple, il n’est pas possible de transformer un champ de type texte comme models.TextField() en un champ de type numérique comme models.IntegerField() dans la plupart des bases de données.

RenameField

class RenameField(model_name, old_name, new_name)

Modifie le nom d’un champ (et, si db_column n’est pas défini, le nom de la colonne).

AddIndex

class AddIndex(model_name, index)

Crée un index dans la table de base de données pour le modèle model_name. index est une instance de la classe Index.

RemoveIndex

class RemoveIndex(model_name, name)

Supprime l’index nommé name du modèle ayant le nom model_name.

AddConstraint

class AddConstraint(model_name, constraint)

Crée une :doc:` contrainte </ref/models/constraints>` dans la table de base de données pour le modèle nommé model_name.

RemoveConstraint

class RemoveConstraint(model_name, name)

Supprime la contrainte nommée name du modèle ayant le nom model_name.

Opérations spéciales

RunSQL

class RunSQL(sql, reverse_sql=None, state_operations=None, hints=None, elidable=False)

Permet d’exécuter du code SQL arbitraire dans la base de données, pratique pour des fonctionnalités plus avancées des moteurs de base de données que Django ne prend pas directement en charge.

sql, et reverse_sql s’il est fourni, doivent être des chaîne de SQL à exécuter dans la base de données. Pour la plupart des moteurs de base de données (tous sauf PostgreSQL), Django sépare le code SQL en instructions individuelles avant de les exécuter.

Avertissement

Avec PostgreSQL et SQLite, n’utilisez des instructions SQL BEGIN ou COMMIT que dans des migrations non atomiques, afin d’éviter de casser l’état de transaction de Django.

Vous pouvez aussi transmettre une liste de chaînes ou de tuples binaires. Cette dernière option est utilisée pour transmettre des requêtes et des paramètres sur le même principe que pour cursor.execute(). Ces trois opérations sont équivalentes :

migrations.RunSQL("INSERT INTO musician (name) VALUES ('Reinhardt');")
migrations.RunSQL([("INSERT INTO musician (name) VALUES ('Reinhardt');", None)])
migrations.RunSQL([("INSERT INTO musician (name) VALUES (%s);", ['Reinhardt'])])

Si vous voulez inclure des signes « pour cent » littéraux dans la requête, vous devez les doubler si vous transmettez des paramètres.

Les requêtes reverse_sql sont exécutées lorsque la migration est défaite. Elles sont censé défaire ce que font les requêtes sql. Par exemple, pour annuler l’insertion ci-dessus par une suppression :

migrations.RunSQL(
    sql=[("INSERT INTO musician (name) VALUES (%s);", ['Reinhardt'])],
    reverse_sql=[("DELETE FROM musician where name=%s;", ['Reinhardt'])],
)

Si reverse_sql vaut None (par défaut), l’opération RunSQL est irréversible.

Le paramètre state_operations permet de fournir des opérations qui sont équivalentes au code SQL en terme d’état du projet. Par exemple, si vous créez une colonne manuellement, vous devriez passer une liste contenant une opération AddField afin que l’autodétecteur puisse maintenir un état à jour du modèle. Sinon, au prochain lancement de makemigrations, il ne verra aucune opération ajoutant ce champ et va donc essayer de le recréer une nouvelle fois. Par exemple :

migrations.RunSQL(
    "ALTER TABLE musician ADD COLUMN name varchar(255) NOT NULL;",
    state_operations=[
        migrations.AddField(
            'musician',
            'name',
            models.CharField(max_length=255),
        ),
    ],
)

Le paramètre facultatif hints sera transmis comme **hints à la méthode allow_migrate() des routeurs de base de données afin de les aider à prendre les décisions de routage. Voir Indications (hints) pour plus de détails sur les indications de base de données.

Le paramètre facultatif elidable détermine si l’opération sera supprimée (éludée) lors de la fusion des migrations.

RunSQL.noop

Transmettez l’attribut RunSQL.noop à sql ou reverse_sql lorsque vous voulez que l’opération ne fasse rien dans une direction donnée. C’est particulièrement utile pour rendre une opération réversible.

RunPython

class RunPython(code, reverse_code=None, atomic=None, hints=None, elidable=False)

Exécute du code Python personnalisé dans un contexte historisé. code (et reverse_code s’il est renseigné) doivent être des objets exécutables acceptant deux paramètres ; le premier est une instance de django.apps.registry.Apps contenant les modèles historisés qui correspondent à la position de l’opération dans l’historique du projet et la seconde une instance de SchemaEditor.

Le paramètre reverse_code est appelé lors de l’inversion des migrations. Cet objet exécutable doit défaire ce qui a été fait par l’objet exécutable code afin que cette migration soit réversible. Si reverse_code vaut None (par défaut), l’opération RunPython est irréversible.

Le paramètre facultatif hints sera transmis comme **hints à la méthode allow_migrate() des routeurs de base de données afin de les aider à prendre une décision de routage. Voir Indications (hints) pour plus de détails sur les indications de base de données.

Le paramètre facultatif elidable détermine si l’opération sera supprimée (éludée) lors de la fusion des migrations.

Il est conseillé d’écrire le code sous forme de fonction distincte au-dessus de la classe Migration dans le fichier de migration et de transmettre celle-ci à RunPython. Voici un exemple d’emploi de RunPython pour créer quelques objets initiaux d’un modèle Country:

from django.db import migrations

def forwards_func(apps, schema_editor):
    # We get the model from the versioned app registry;
    # if we directly import it, it'll be the wrong version
    Country = apps.get_model("myapp", "Country")
    db_alias = schema_editor.connection.alias
    Country.objects.using(db_alias).bulk_create([
        Country(name="USA", code="us"),
        Country(name="France", code="fr"),
    ])

def reverse_func(apps, schema_editor):
    # forwards_func() creates two Country instances,
    # so reverse_func() should delete them.
    Country = apps.get_model("myapp", "Country")
    db_alias = schema_editor.connection.alias
    Country.objects.using(db_alias).filter(name="USA", code="us").delete()
    Country.objects.using(db_alias).filter(name="France", code="fr").delete()

class Migration(migrations.Migration):

    dependencies = []

    operations = [
        migrations.RunPython(forwards_func, reverse_func),
    ]

Il s’agit généralement de l’opération que l’on utilise pour créer des migrations de données, pour lancer des mises à jour et modifications de données personnalisées ou pour toute autre opération qui nécessite le recours à l’ORM ou à du code Python.

Tout comme pour RunSQL, si vous faites ici des modifications au schéma, prenez soin de le faire en dehors de la portée du systèmes des modèles Django (par ex. avec des déclencheurs) ou utilisez SeparateDatabaseAndState pour insérer des opérations qui reflètent vos modifications sur l’état des modèles. Sinon, l’ORM versionné et l’autodétecteur risquent de ne plus fonctionner correctement.

Par défaut, RunPython va exécuter son contenu à l’intérieur d’une transaction pour les bases de données qui ne prennent pas en charge les transactions DDL (instructions de modification du schéma), par exemple MySQL ou Oracle. Cela se passe en principe bien, mais pourrait provoquer un plantage si vous essayez d’utiliser l’objet schema_editor fournit par ces moteurs ; dans ce cas, passez atomic=False à l’opération RunPython.

Pour les bases de données qui prennent en charge les transactions de schéma DDL (SQLite et PostgreSQL), aucune transaction n’est automatiquement rajoutée pour les opérations RunPython en dehors des transactions créées pour chaque migration. Ainsi, par exemple avec PostgreSQL, vous devriez éviter de combiner des modifications de schéma avec des opérations RunPython dans la même migration, au risque de recevoir des erreurs du genre OperationalError: cannot ALTER TABLE "mytable" because it has pending trigger events.

Si votre base de données est différente et que vous n’êtes pas sûr si elle prend en charge les transactions DDL, consultez l’attribut django.db.connection.features.can_rollback_ddl.

Si l’opération RunPython fait partie d’une migration non atomique, l’opération ne sera exécutée dans une transaction que si atomic=True est transmis à l’opération RunPython.

Avertissement

RunPython ne modifie pas de façon magique la connexion des modèles à votre place ; toute méthode de modèle que vous appelez sera dirigée vers la base de données par défaut, sauf si vous leur indiquez l’alias de base de données à utiliser (disponible dans schema_editor.connection.alias, où schema_editor est le second paramètre reçu par votre fonction).

static RunPython.noop()

Transmettez la méthode RunPython.noop à code ou reverse_code lorsque vous voulez que l’opération ne fasse rien dans une direction donnée. C’est particulièrement utile pour rendre une opération réversible.

SeparateDatabaseAndState

class SeparateDatabaseAndState(database_operations=None, state_operations=None)

Une opération hautement spécialisée qui permet de mélanger des opérations touchant aux aspects de modification de schéma de base de données avec des opérations de changement d’état (en lien avec l’autodétecteur).

Elle accepte deux listes d’opérations. Quand on lui demande d’appliquer les changements d’état, elle utilise la liste state_operations (il s’agit d’une version généralisée du paramètre state_operations de RunSQL). Quand on lui demande d’appliquer les modifications de base de données, elle utilise la liste database_operations.

Si l’état actuel de la base de données et la vue de cet état par Django diffèrent, ceci peut casser le système des migrations, voire même aboutir à des pertes de données. Il vaut la peine de faire bien attention et de contrôler attentivement les opérations de base de données et d’état. Vous pouvez utiliser sqlmigrate et dbshell pour vérifier les opérations de base de données. Vous pouvez utiliser makemigrations, particulièrement avec l’option --dry-run, pour vérifier les opérations d’état.

Pour voir un exemple qui utilise SeparateDatabaseAndState, consultez Modification d’un champ ManyToManyField pour utiliser un modèle intermédiaire.

Écriture d’opérations personnalisées

Les opérations possèdent une API relativement simple et sont conçues pour pouvoir facilement écrire des sous-classes pour compléter celles que Django fournit. La structure de base d’une Operation ressemble à ceci :

from django.db.migrations.operations.base import Operation

class MyCustomOperation(Operation):

    # If this is False, it means that this operation will be ignored by
    # sqlmigrate; if true, it will be run and the SQL collected for its output.
    reduces_to_sql = False

    # If this is False, Django will refuse to reverse past this operation.
    reversible = False

    def __init__(self, arg1, arg2):
        # Operations are usually instantiated with arguments in migration
        # files. Store the values of them on self for later use.
        pass

    def state_forwards(self, app_label, state):
        # The Operation should take the 'state' parameter (an instance of
        # django.db.migrations.state.ProjectState) and mutate it to match
        # any schema changes that have occurred.
        pass

    def database_forwards(self, app_label, schema_editor, from_state, to_state):
        # The Operation should use schema_editor to apply any changes it
        # wants to make to the database.
        pass

    def database_backwards(self, app_label, schema_editor, from_state, to_state):
        # If reversible is True, this is called when the operation is reversed.
        pass

    def describe(self):
        # This is used to describe what the operation does in console output.
        return "Custom Operation"

    @property
    def migration_name_fragment(self):
        # Optional. A filename part suitable for automatically naming a
        # migration containing this operation, or None if not applicable.
        return "custom_operation_%s_%s" % (self.arg1, self.arg2)
New in Django 3.2:

La propriété migration_name_fragment a été ajoutée.

Vous pouvez prendre ce modèle et l’étendre, même si nous suggérons d’examiner les opérations fournies par Django dans django.db.migrations.operations. Elles couvrent une grande partie des exemples d’utilisation des aspects semi-internes du système des migrations, tels que ProjectState ainsi que les mécanismes utilisés pour obtenir les modèles historisés, tout comme ModelState et les motifs utilisés pour faire évoluer les modèles historisés dans state_forwards().

Quelques éléments à signaler :

  • Vous n’avez pas besoin d’en apprendre beaucoup sur ProjectState pour écrire des migrations ; il suffit de savoir qu’il possède une propriété apps qui donne accès à un registre d’applications (sur lequel vous pouvez appeler get_model).

  • database_forwards et database_backwards reçoivent tous deux deux états ; ils représentent la différence par rapport à ce que la méthode state_forwards aurait appliqué, mais vous sont transmis par commodité et pour des raisons de vitesse.

  • Si vous souhaitez manipuler des classes ou instances de modèles à partir du paramètre from_state dans database_forwards() ou database_backwards(), il faut produire les états de modèles en utilisant la méthode clear_delayed_apps_cache() afin de rendre disponible les modèles liés :

    def database_forwards(self, app_label, schema_editor, from_state, to_state):
        # This operation should have access to all models. Ensure that all models are
        # reloaded in case any are delayed.
        from_state.clear_delayed_apps_cache()
        ...
    
  • to_state dans la méthode database_backwards est l’état plus ancien ; c’est-à-dire celui qui représentera l’état actuel au moment où la migration aura terminé son travail d’annulation.

  • Vous pouvez trouver des implémentations de references_model dans les opérations fournies par Django ; cela fait partie du code d’autodétection et ne joue pas de rôle pour les opérations personnalisées.

Avertissement

Pour des raisons de performance, les instances Field dans ModelState.fields sont réutilisées d’une migration à l’autre. Vous ne devez jamais toucher aux attributs de ces instances. Si vous avez besoin de faire évoluer un champ dans state_forwards(), vous devez enlever l’ancienne instance de ModelState.fields et ajouter une nouvelle instance à sa place. Le même principe vaut pour les instances Manager dans ModelState.managers.

Comme exemple, créons une opération qui charge des extensions PostgreSQL (qui contiennent certaines des fonctionnalités les plus enthousiasmantes de PostgreSQL. Comme il n’y a pas de changement d’état de modèle, tout ce qu’elle fait, c’est d’exécuter une commande :

from django.db.migrations.operations.base import Operation

class LoadExtension(Operation):

    reversible = True

    def __init__(self, name):
        self.name = name

    def state_forwards(self, app_label, state):
        pass

    def database_forwards(self, app_label, schema_editor, from_state, to_state):
        schema_editor.execute("CREATE EXTENSION IF NOT EXISTS %s" % self.name)

    def database_backwards(self, app_label, schema_editor, from_state, to_state):
        schema_editor.execute("DROP EXTENSION %s" % self.name)

    def describe(self):
        return "Creates extension %s" % self.name

    @property
    def migration_name_fragment(self):
        return "create_extension_%s" % self.name
Back to Top