Sith/docs/howto/migrations.md
2024-07-21 00:56:58 +02:00

10 KiB

Qu'est-ce qu'une migration ?

Une migration est un fichier Python qui contient des instructions pour modifier la base de données. Une base de données évolue au cours du temps, et les migrations permettent de garder une trace de ces modifications.

Grâce à elles, on peut également apporter des modifications à la base de données sans être obligées de la recréer. On applique seulement les modifications nécessaires.

Appliquer les migrations

Pour appliquer les migrations, exécutez la commande suivante :

python ./manage.py migrate

Vous remarquerez peut-être que cette commande a été utilisée dans la section Installation. En effet, en partant d'une base de données vierge et en appliquant toutes les migrations, on arrive à l'état actuel de la base de données. Logique.

Si vous utilisez cette commande sur une base de données sur laquelle toutes les migrations ont été appliquées, elle ne fera rien.

Si vous utilisez cette commande sur une base de données sur laquelle seule une partie des migrations ont été appliquées, seules les migrations manquantes seront appliquées.

Créer une migration

Pour créer une migration, exécutez la commande suivante :

python ./manage.py makemigrations

Cette commande comparera automatiquement le contenu des classes de modèles et le comparera avec les migrations déjà appliquées. A partir de cette comparaison, elle générera automatiquement une nouvelle migration.

!!! note

La commande `makemigrations` ne fait que
générer les fichiers de migration.
Elle ne modifie pas la base de données.
Pour appliquer la migration, n'oubliez pas la
commande `migrate`.

Un fichier de migration ressemble à ça :

from django.db import migrations

    
class Migration(migrations.Migration):

    dependencies = [
        # liste des autres migrations à appliquer avant celle-ci
    ]

    operations = [
        # liste des opérations à appliquer sur la db
    ]

Grâce à la liste des dépendances, Django sait dans quel ordre les migrations doivent être appliquées. Grâce à la liste des opérations, Django sait quelles sont les opérations à appliquer durant cette migration.

Revenir à une migration antérieure

Lorsque vous développez, il peut arriver que vous vouliez revenir à une migration antérieure. Pour cela, il suffit d'appliquer la commande migrate en spécifiant le nom de la migration à laquelle vous voulez revenir :

python ./manage.py migrate <application> <numéro de la migration>

Par exemple, si vous voulez revenir à la migration 0001_initial de l'application customer, vous pouvez exécuter la commande suivante :

python ./manage.py migrate customer 0001

Customiser une migration

Il peut arriver que vous ayez besoin de modifier le fichier de migration généré par Django. Par exemple, si vous voulez exécuter un script Python lors de l'application de la migration.

Dans ce cas, vous pouvez trouver les fichiers de migration dans le dossier migrations de chaque application. Vous pouvez modifier le fichier Python correspondant à la migration que vous voulez modifier.

Ajoutez l'opération que vous voulez effectuer dans l'attribut operations de la classe Migration.

Par exemple :

from django.db import migrations

def forwards_func(apps, schema_editor):
    print("Appplication de la migration")

def reverse_func(apps, schema_editor):
    print("Annulation de la migration")
    
class Migration(migrations.Migration):

    dependencies = []

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

!!! warning "Script d'annulation de la migration"

Lorsque vous incluez un script Python dans une migration,
incluez toujours aussi un script d'annulation,
sinon Django ne pourra pas annuler la migration
après son application.

Vous ne pourrez donc pas revenir à un état antérieur
de la db, à moins de la recréer de zéro.

Fusionner des migrations

Quand on travaille sur une fonctionnalité qui nécessite une modification de la base de données, les fichiers de migration sont comme toute chose : on peut se rendre compte que les changements apportés pourraient être meilleurs.

Par exemple, supposons que nous voulons créer un modèle représentant une UE suivie par un étudiant (ne demandez pas pourquoi on voudrait faire ça, c'est juste pour l'exemple). Un tel modèle aurait besoin des informations suivantes :

  • l'utilisateur
  • le code de l'UE

On écrirait donc, dans l'application pedagogy :

from django.db import models
from core.models import User

class UserUe(models.Model):
    user = models.ForeignKey(User, on_delete=models.CASCADE)
    ue = models.CharField(max_length=10)

Et nous aurions le fichier de migration suivant :

from django.db import migrations, models
from django.conf import settings

class Migration(migrations.Migration):
    dependencies = [
        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
        ("pedagogy", "0003_alter_uv_language"),
    ]

    operations = [
        migrations.CreateModel(
            name="UserUe",
            fields=[
                (
                    "id",
                    models.AutoField(
                        auto_created=True,
                        primary_key=True,
                        serialize=False,
                        verbose_name="ID",
                    ),
                ),
                ("ue", models.CharField(max_length=10)),
                (
                    "user",
                    models.ForeignKey(
                        on_delete=models.deletion.CASCADE,
                        to=settings.AUTH_USER_MODEL,
                    ),
                ),
            ],
        ),
    ]

On finit son travail, on soumet la PR. Mais là, quelqu'un fait remarquer qu'il existe déjà un modèle pour représenter une UE. On modifie donc le modèle :

from django.db import models
from core.models import User
from pedagogy.models import UV

class UserUe(models.Model):
    user = models.ForeignKey(User, on_delete=models.CASCADE)
    ue = models.ForeignKey(UV, on_delete=models.CASCADE)

On refait la commande makemigrations et on obtient :

from django.db import migrations, models


class Migration(migrations.Migration):
    dependencies = [
        ("pedagogy", "0004_userue"),
    ]

    operations = [
        migrations.AlterField(
            model_name="userue",
            name="ue",
            field=models.ForeignKey(
                on_delete=models.deletion.CASCADE, to="pedagogy.uv"
            ),
        ),
    ]

Sauf que maintenant, nous avons deux fichiers de migration, alors qu'en réalité, on ne souhaite faire qu'une seule migration, une fois qu'on aura expédié le code en prod. Certes, ça fonctionnerait d'appliquer les deux, mais ça pose un problème d'encombrement.

Plus il y a de fichiers de migrations, plus il y a de migrations à résoudre au moment de l'installation du projet chez quelqu'un, plus c'est embêtant à gérer et plus Django prendra du temps à résoudre les migrations.

C'est pourquoi il est bon de respecter le principe : une PR = un fichier de migration maximum par application.

Nous voulons donc fusionner les deux, pour n'en garder qu'une. Pour ça, deux manières de procéder :

  • le faire à la main
  • utiliser la commande squashmigrations

Pour la méthode manuelle, on ne pourrait pas vous dire exhaustivement comment faire. Mais ne vous inquiétez pas, ce n'est pas très dur. Regardez bien quelles sont les instructions utilisées par django pour les opérations de migrations, et avec un peu d'astuce et quelques copier-coller, vous vous en sortirez comme des chefs.

Pour la méthode squashmigrations, exécutez la commande

python ./manage.py squasmigrations <app> <migration de début (incluse)> <migration de fin (incluse)> 

Par exemple, dans notre cas, ça donnera :

python ./manage.py squasmigrations pedagogy 0004 0005 

La commande vous donnera ceci :

from django.conf import settings
from django.db import migrations, models

class Migration(migrations.Migration):
    replaces = [("pedagogy", "0004_userue"), ("pedagogy", "0005_alter_userue_ue")]

    dependencies = [
        ("pedagogy", "0003_alter_uv_language"),
        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
    ]

    operations = [
        migrations.CreateModel(
            name="UserUe",
            fields=[
                (
                    "id",
                    models.AutoField(
                        auto_created=True,
                        primary_key=True,
                        serialize=False,
                        verbose_name="ID",
                    ),
                ),
                (
                    "ue",
                    models.ForeignKey(
                        on_delete=models.deletion.CASCADE, to="pedagogy.uv"
                    ),
                ),
                (
                    "user",
                    models.ForeignKey(
                        on_delete=models.deletion.CASCADE,
                        to=settings.AUTH_USER_MODEL,
                    ),
                ),
            ],
        ),
    ]

Vous pouvez alors supprimer les deux autres fichiers.

Vous remarquerez peut-être la présence de la ligne suivante :

replaces = [("pedagogy", "0004_userue"), ("pedagogy", "0005_alter_userue_ue")]

Cela sert à dire que cette migration doit être appliquée à la place des deux autres. Une fois que vous aurez supprimé les deux fichiers, supprimez également cette ligne.

!!!warning

Django sait quelles migrations ont été appliquées,
en les stockant dans une table de la db.
Si une migration est enregistrée en db, sans que le fichier
de migration correspondant existe,
la commande `migrate` échoue.

Quand vous faites un `squashmigrations`,
pensez donc à appliquer la commande `migrate`
juste après (mais avant la suppression des anciens fichiers),
pour que Django supprime de la base de données
les migrations devenues inutiles.