Avec Django, la modélisation est en lien direct avec la conception et le stockage, sous forme d'une base de données relationnelle, et la manière dont ces données s'agencent et communiquent entre elles.
Django utilise un paradigme de persistence des données de type https://fr.wikipedia.org/wiki/Mapping_objet-relationnel[ORM] - c'est-à-dire que chaque type d'objet manipulé peut s'apparenter à une table SQL, tout en respectant une approche propre à la programmation orientée object.
Plus spécifiquement, l'ORM de Django suit le patron de conception https://en.wikipedia.org/wiki/Active_record_pattern[Active Records], comme le font par exemple https://rubyonrails.org/[Rails] pour Ruby ou https://docs.microsoft.com/fr-fr/ef/[EntityFramework] pour .Net.
Le modèle de données de Django est sans doute la (seule ?) partie qui soit tellement couplée au framework qu'un changement à ce niveau nécessitera une refonte complète de beaucoup d'autres briques de vos applications; là où un pattern de type https://www.martinfowler.com/eaaCatalog/repository.html[Repository] permettrait justement de découpler le modèle des données de l'accès à ces mêmes données, un pattern Active Record lie de manière extrêmement forte le modèle à sa persistence.
Architecturalement, c'est sans doute la plus grosse faiblesse de Django, à tel point que *ne pas utiliser cette brique de fonctionnalités* peut remettre en question le choix du framework.
Conceptuellement, c'est pourtant la manière de faire qui permettra d'avoir quelque chose à présenter très rapidement: à partir du moment où vous aurez un modèle de données, vous aurez accès, grâce à cet ORM à:
Comme tout ceci reste au niveau du code, cela suit également la méthodologie des douze facteurs, concernant la minimisation des divergences entre environnements d'exécution: comme tout se trouve au niveau du code, il n'est plus nécessaire d'avoir un DBA qui doive démarrer un script sur un serveur au moment de la mise à jour, de recevoir une release note de 512 pages en PDF reprenant les modifications ou de nécessiter l'intervention de trois équipes différentes lors d'une modification majeure du code.
Déployer une nouvelle instance de l'application pourra être réalisé directement à partir d'une seule et même commande.
Il faut noter que l'implémentation d'Active Records reste une forme hybride entre une structure de données brutes et une classe: là où une classe va exposer ses données derrière une forme d'abstraction et n'exposer que les fonctions qui opèrent sur ces données, une structure de données ne va exposer que ses champs et propriétés, et ne va pas avoir de functions significatives.
Dans l'exemple géométrique ci-dessus, repris de cite:[clean_code, 95-97], l'accessibilité des champs devient restreinte, tandis que la fonction `area()` bascule comme méthode d'instance plutôt que de l'isoler au niveau d'un visiteur.
Nous ajoutons une abstraction au niveau des formes grâce à un héritage sur la classe `Shape`; indépendamment de ce que nous manipulerons, nous aurons la possibilité de calculer son aire.
Une structure de données permet de facilement gérer des champs et des propriétés, tandis qu'une classe gère et facilite l'ajout de fonctions et de méthodes.
Le problème d'Active Records est que chaque classe s'apparente à une table SQL et revient donc à gérer des _DTO_ ou _Data Transfer Object_, c'est-à-dire des objets de correspondance pure et simple entre les champs de la base de données et les propriétés de la programmation orientée objet, c'est-à-dire également des classes sans fonctions.
Or, chaque classe a également la possibilité d'exposer des possibilités d'interactions au niveau de la persistence, en https://docs.djangoproject.com/en/stable/ref/models/instances/#django.db.models.Model.save[enregistrant ses propres données] ou en en autorisant leur https://docs.djangoproject.com/en/stable/ref/models/instances/#deleting-objects[suppression].
Nous arrivons alors à un modèle hybride, mélangeant des structures de données et des classes d'abstraction, ce qui restera parfaitement viable tant que l'on garde ces principes en tête et que l'on se prépare à une éventuelle réécriture du code.
Lors de l'analyse d'une classe de modèle, nous pouvons voir que Django exige un héritage de la classe `django.db.models.Model`.
Nous pouvons regarder les propriétés définies dans cette classe en analysant le fichier `lib\site-packages\django\models\base.py`.
Outre que `models.Model` hérite de `ModelBase` au travers de https://pypi.python.org/pypi/six[six] pour la rétrocompatibilité vers Python 2.7, cet héritage apporte notamment les fonctions `save()`, `clean()`, `delete()`, ...
Par défaut, et si aucune propriété ne dispose d'un attribut `primary_key=True`, Django s'occupera d'ajouter un champ `id` grâce à son héritage de la classe `models.Model`.
Les autres champs nous permettent d'identifier une catégorie (`Category`) par un nom, tandis qu'un livre (`Book`) le sera par ses propriétés `title` et une clé de relation vers une catégorie.
Un livre est donc lié à une catégorie, tandis qu'une catégorie est associée à plusieurs livres.
Nous nous rendons rapidement compte qu'un livre peut appartenir à plusieurs catégories: _Dune_ a été adapté au cinéma en 1973 et en 2021, de même que _Le Seigneur des Anneaux_, _The Great Gatsby_, et sans doute que nous pourrons étoffer notre bibliothèque avec une catégorie spéciale "Baguettes magiques et trucs phalliques", à laquelle nous pourrons associer la saga _Harry Potter_.
En clair, notre modèle n'est pas adapté, et nous devons le modifier pour que notre clé étrangère accepte plusieurs valeurs.
Ceci peut être fait au travers d'un champ de type `ManyToMany`, c'est-à-dire qu'un livre peut être lié à plusieurs catégories, et qu'une catégorie peut être liée à plusieurs livres.
Depuis le code, à partir de l'instance de la classe `Item`, on peut donc accéder à la liste en appelant la propriété `wishlist` de notre instance. *A contrario*, depuis une instance de type `Wishlist`, on peut accéder à tous les éléments liés grâce à `<nom de la propriété>_set`; ici `item_set`.
Lorsque vous déclarez une relation 1-1, 1-N ou N-N entre deux classes, vous pouvez ajouter l'attribut `related_name` afin de nommer la relation inverse.
Dans les examples ci-dessus, nous avons vu les relations multiples (1-N), représentées par des clés étrangères (**ForeignKey**) d'une classe A vers une classe B.
Pour représenter d'autres types de relations, il existe également les champs de type *ManyToManyField*, afin de représenter une relation N-N.
Il existe également un type de champ spécial pour les clés étrangères, qui est le Les champs de type *OneToOneField*, pour représenter une relation 1-1.
Comme chaque classe héritant de `models.Model` possède une propriété `objects`. Comme on l'a vu dans la section **Jouons un peu avec la console**, cette propriété permet d'accéder aux objects persistants dans la base de données, au travers d'un `ModelManager`.
En plus de cela, il faut bien tenir compte des propriétés `Meta` de la classe: si elle contient déjà un ordre par défaut, celui-ci sera pris en compte pour l'ensemble des requêtes effectuées sur cette classe.
<1> Nous définissons un ordre par défaut, directement au niveau du modèle. Cela ne signifie pas qu'il ne sera pas possible de modifier cet ordre (la méthode `order_by` existe et peut être chaînée à n'importe quel _queryset_). D'où l'intérêt de tester ce type de comportement, dans la mesure où un `top 1` dans votre code pourrait être modifié simplement par cette petite information.
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
start_zone = models.PositiveSmallIntegerField(choices=Zone.choices, default=Zone.ZONE_5, help_text="What was your best time on the marathon in last 2 years?") # this is new
Si vous décidez de définir un constructeur sur votre modèle, ne surchargez pas la méthode `__init__`: créez plutôt une méthode static de type `create()`, en y associant les paramètres obligatoires ou souhaités: