gwift-book/source/part-3-data-model/migrations.adoc

116 lines
6.0 KiB
Plaintext
Raw Normal View History

== Migrations
L'intégration des migrations a été réalisée dans la version 1.7 de Django.
Avant cela, il convenait de passer par une librairie tierce intitulée https://south.readthedocs.io/en/latest[South].
Dans cette section, nous allons voir comment fonctionnent les migrations.
Lors d'une première approche, elles peuvent sembler un peu magiques, puisqu'elles centralisent un ensemble de modifications pouvant être répétées sur un schéma de données, en tenant compte de ce qui a déjà été appliqué et en vérifiant quelles migrations devaient encore l'être pour mettre l'application à niveau.
Une analyse en profondeur montrera qu'elles ne sont pas plus complexes à suivre et à comprendre qu'un ensemble de fonctions de gestion appliquées à notre application.
Prenons l'exemple de notre liste de souhaits; nous nous rendons (bêtement) compte que nous avons oublié d'ajouter un champ de `description` à une liste.
Historiquement, cette action nécessitait l'intervention d'un administrateur système ou d'une personne ayant accès au schéma de la base de données, à partir duquel ce-dit utilisateur pouvait jouer manuellement un script SQL.
Cet enchaînement d'étapes nécessitait une bonne coordination d'équipe, mais également une bonne confiance dans les scripts à exécuter.
Et souvenez-vous (cf. ref-à-insérer), que l'ensemble des actions doit être répétable et automatisable.
Bref, dans les années '80, il convenait de jouer ceci après s'être connecté à la base de données:
[source,sql]
----
ALTER TABLE WishList ADD COLUMN Description nvarchar(MAX);
----
Et là, nous nous rappelons qu'un utilisateur tourne sur Oracle et pas sur MySQL, et qu'il a donc besoin de son propre script d'exécution, parce que le type du nouveau champ n'est pas exactement le même entre les deux moteurs:
[source,sql]
----
----
Bref, vous voyez le(s) problème(s):
1. Aucune autonomie
2. Aucune automatisation possible (à moins d'écrire un programme, qu'il faudra également maintenir et intégrer au niveau des tests)
3. Nécessiter de maintenir autant de scripts différents qu'il y a de moteurs de base de données supportés
4. Aucune possibilité de vérifier si le script a déjà été exécuté ou non (à moins de maintenir un programme supplémentaire, à nouveau)
5. ...
Les migrations résolvent la plupart de ces soucis: le framework embarque ses propres applications, dont les migrations, qui gèrent elles-mêmes l'arbre de dépendances entre les modifications devant être appliquées.
Une migration consiste donc à appliquer un ensemble de modifications (ou **opérations**), qui exercent un ensemble de transformations, pour que le schéma de base de données corresponde au modèle de l'application sous-jacente.
Les migrations (comprendre les "_migrations du schéma de base de données_") sont intimement liées à la représentation d'un contexte fonctionnel.
L'ajout d'une nouvelle information, d'un nouveau champ ou d'une nouvelle fonction peut s'accompagner de tables de données à mettre à jour ou de champs à étendre.
Toujours dans une optique de centralisation, les migrations sont directement embarquées au niveau du code.
Le développeur s'occupe de créer les migrations en fonction des actions à entreprendre; ces migrations peuvent être retravaillées, _squashées_, ... et feront partie intégrante du processus de mise à jour de l'application.
A noter que les migrations n'appliqueront de modifications que si le schéma est impacté.
Ajouter une propriété `related_name` sur une ForeignKey n'engendrera aucune nouvelle action de migration, puisque ce type d'action ne s'applique que sur l'ORM, et pas directement sur la base de données: au niveau des tables, rien ne change.
Seul le code et le modèle sont impactés.
Une migration est donc une classe Python, présentant _a minima_ deux propriétés:
1. `dependencies`, qui décrit les opérations précédentes devant obligatoirement avoir été appliquées
2. `operations`, qui consiste à décrire précisément ce qui doit être exécuté.
Pour reprendre notre exemple d'ajout d'un champ `description` sur le modèle `WishList`, la migration ressemblera à ceci:
[source,python]
----
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
class Migration(migrations.Migration):
dependencies = [
('gwift', '0004_name_value'),
]
operations = [
migrations.AddField(
model_name='wishlist',
name='description',
field=models.TextField(default="", null=True)
preserve_default=False,
),
]
----
Nous avions un modèle reprenant quelques classes, elles-mêmes saupoudrées de quelques propriétés.
=== Réinitialisation d'une ou plusieurs migrations
https://simpleisbetterthancomplex.com/tutorial/2016/07/26/how-to-reset-migrations.html[reset migrations].
> En gros, soit on supprime toutes les migrations (en conservant le fichier __init__.py), soit on réinitialise proprement les migrations avec un --fake-initial (sous réserve que toutes les personnes qui utilisent déjà le projet s'y conforment... Ce qui n'est pas gagné.
Pour repartir de notre exemple ci-dessus, nous avions un modèle reprenant quelques classes, saupoudrées de propriétés décrivant nos différents champs. Pour être prise en compte par le moteur de base de données, chaque modification doit être
=== Description d'une migration
1. Décrite, grâce à la commande `makemigrations`
=== Application d'une ou plusieurs migrations
1. Appliquée, avec la commande `migrate`.
=== Analyse
Nous allons ci-dessous analyser exactement les modifications appliquées au schéma de la base de données, en fonction des différents cas, et comment ils sont gérés par les pilotes de Django.
Nous utiliserons https://sqlitebrowser.org/[Sqlite Browser] et la commande `sqldump`, qui nous présentera le schéma tel qu'il sera compris.
==== Création de nouveaux champs
==== Modification d'un champ existant
==== Suppression d'un champ existant