Add the dependency graph within migrations
continuous-integration/drone/push Build is passing Details

This commit is contained in:
Fred Pauchet 2022-03-13 19:24:19 +01:00
parent 3370f5b20c
commit 850c0381aa
2 changed files with 76 additions and 33 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

View File

@ -1,13 +1,14 @@
== 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.
NOTE: La commande `sqldump`, qui nous présentera le schéma tel qu'il sera compris.
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].
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. (((sql)))
Cet enchaînement d'étapes nécessitait une bonne coordination d'équipe, mais également une bonne confiance dans les scripts à exécuter.
@ -190,6 +191,73 @@ Nous obtenons à présent la représentation suivante en base de données:
image::images/db/link-book-category-m2m.drawio.png[]
=== Graph de dépendances
Lorsqu'une migration applique une modification au schéma d'une base de données, il est évident qu'elle ne peut pas être appliquée dans n'importe quel ordre ou à n'importe quel moment.
Dès la création d'un nouveau projet, avec une configuration par défaut et même sans avoir ajouté d'applications, Django proposera immédiatement d'appliquer les migrations des applications **admin**, **auth**, **contenttypes** et **sessions**, qui font partie du coeur du système, et qui se trouvent respectivement aux emplacements suivants:
* *admin*: `site-packages/django/contrib/admin/migrations`
* *auth*: `site-packages/django/contrib/auth/migrations`
* *contenttypes*: `site-packages/django/contrib/contenttypes/migrations`
* *sessions*: `site-packages/django/contrib/sessions/migrations`
Ceci est dû au fait que, toujours par défaut, ces applications sont reprises au niveau de la configuration d'un nouveau projet, dans le fichier `settings.py`:
[source,python]
----
[snip]
INSTALLED_APPS = [
'django.contrib.admin', <1>
'django.contrib.auth', <2>
'django.contrib.contenttypes', <3>
'django.contrib.sessions', <4>
'django.contrib.messages',
'django.contrib.staticfiles',
]
[snip]
----
<1> Admin
<2> Auth
<3> Contenttypes
<4> et Sessions.
Dès que nous les appliquerons, nous recevrons les messages suivants:
[source,bash]
----
$ python manage.py migrate
Operations to perform:
Apply all migrations: admin, auth, contenttypes, library, sessions, world
Running migrations:
Applying contenttypes.0001_initial... OK
Applying auth.0001_initial... OK
Applying admin.0001_initial... OK
Applying admin.0002_logentry_remove_auto_add... OK
Applying admin.0003_logentry_add_action_flag_choices... OK
Applying contenttypes.0002_remove_content_type_name... OK
Applying auth.0002_alter_permission_name_max_length... OK
Applying auth.0003_alter_user_email_max_length... OK
Applying auth.0004_alter_user_username_opts... OK
Applying auth.0005_alter_user_last_login_null... OK
Applying auth.0006_require_contenttypes_0002... OK
Applying auth.0007_alter_validators_add_error_messages... OK
Applying auth.0008_alter_user_username_max_length... OK
Applying auth.0009_alter_user_last_name_max_length... OK
Applying auth.0010_alter_group_name_max_length... OK
Applying auth.0011_update_proxy_permissions... OK
Applying auth.0012_alter_user_first_name_max_length... OK
Applying sessions.0001_initial... OK
----
Cet ordre est défini au niveau de la propriété `dependencies`, que l'on retrouve au niveau de chaque description de migration,
En explorant les paquets qui se trouvent au niveau des répertoires et en analysant les dépendances décrites au niveau de chaque action de migration, on arrive au schéma suivant, qui est un graph dirigé acyclique:
image::images/db/migrations_auth_admin_contenttypes_sessions.png[]
=== Sous le capot
Une migration consiste à 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.
@ -210,6 +278,10 @@ Traceback (most recent call last):
sqlite3.OperationalError: no such column: library_book.summary
----
Pour éviter ce type d'erreurs, il est impératif que les nouvelles migrations soient appliquées **avant** que le code ne soit déployé; l'idéal étant que ces deux opérations soient réalisées de manière atomique, avec un _rollback_ si une anomalie était détectée.
En allant
Pour éviter ce type d'erreurs, plusieurs stratégies peuvent être appliquées:
intégrer ici un point sur les updates db - voir designing data-intensive applications.
@ -221,7 +293,6 @@ A noter que les migrations n'appliqueront de modifications que si le schéma est
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
@ -252,8 +323,6 @@ class Migration(migrations.Migration):
]
----
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
@ -261,29 +330,3 @@ https://simpleisbetterthancomplex.com/tutorial/2016/07/26/how-to-reset-migration
> 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