== Modélisation

Ce chapitre aborde la modélisation des objets et les options qui y sont liées.

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.
Cette modélisation va ériger les premières pierres de votre édifice

[quote,Aurélie Jean,De l'autre côté de la machine,Une rencontre sans fin]
_Le modèle n'est qu'une grande hypothèque.
Il se base sur des choix conscients et inconscients, et dans chacun de ces choix se cachent nos propres perceptions qui résultent de qui nous sommes, de nos connaissances, de nos profils scientifiques et de tant d'autres choses._

Comme expliqué par Aurélie Jean cite:[other_side], "_toute modélisation reste une approximation de la réalité_".
Plus tard dans ce chapitre, nous expliquerons les bonnes pratiques à suivre pour faire évoluer ces biais.


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 à:

1. Des migrations de données et la possibilité de faire évoluer votre modèle,
2. Une abstraction entre votre modélisation et la manière dont les données sont représentées _via_ un moteur de base de données relationnelles,
3. Une interface d'administration auto-générée
4. Un mécanisme de formulaires HTML qui soit complet, pratique à utiliser, orienté objet et facile à faire évoluer,
5. Une définition des notions d'héritage (tout en restant dans une forme d'héritage simple).

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.

=== Active Records

Il est important de noter que l'implémentation d'Active Records reste une forme hybride entre une structure de données brutes et une classe: 

* 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.

L'exemple ci-dessous présente trois structure de données, qui exposent chacune leurs propres champs:

[source,python]
----
class Square:
    def __init__(self, top_left, side):
        self.top_left = top_left
        self.side = side

class Rectangle:
    def __init__(self, top_left, height, width):
        self.top_left = top_left
        self.height = height
        self.width = width

class Circle:
    def __init__(self, center, radius):
        self.center = center
        self.radius = radius
----

Si nous souhaitons ajouter une fonctionnalité permettant de calculer l'aire pour chacune de ces structures, nous aurons deux possibilités:

1. Soit ajouter une classe de _visite_ qui ajoute cette fonction de calcul d'aire
2. Soit modifier notre modèle pour que chaque structure hérite d'une classe de type `Shape`, qui implémentera elle-même ce calcul d'aire.

Dans le premier cas, nous pouvons procéder de la manière suivante:

[source,python]
----
class Geometry:
    PI = 3.141592653589793

    def area(self, shape):
        if isinstance(shape, Square):
            return shape.side * shape.side

        if isinstance(shape, Rectangle):
            return shape.height * shape.width

        if isinstance(shape, Circle):
            return PI * shape.radius**2

        raise NoSuchShapeException()
----

Dans le second cas, l'implémentation pourrait évoluer de la manière suivante:

[source,python]
----
class Shape:
    def area(self):
        pass

class Square(Shape):
    def __init__(self, top_left, side):
        self.__top_left = top_left
        self.__side = side

    def area(self):
        return self.__side * self.__side

class Rectangle(Shape):
    def __init__(self, top_left, height, width):
        self.__top_left = top_left
        self.__height = height
        self.__width = width

    def area(self):
        return self.__height * self.__width

class Circle(Shape):
    def __init__(self, center, radius):
        self.__center = center
        self.__radius = radius

    def area(self):
        PI = 3.141592653589793
        return PI * self.__radius**2
----

Une structure de données peut être rendue abstraite au travers des notions de programmation orientée objet.

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()`, ...
En résumé, toutes les méthodes qui font qu'une instance sait **comment** interagir avec la base de données.

=== Types de champs, relations et clés étrangères

Nous l'avons vu plus tôt, Python est un langage dynamique et fortement typé.
Django, de son côté, ajoute une couche de typage statique exigé par le lien sous-jacent avec le moteur de base de données relationnelle.
Dans le domaine des bases de données relationnelles, un point d'attention est de toujours disposer d'une clé primaire pour nos enregistrements.
Si aucune clé primaire n'est spécifiée, Django s'occupera d'en ajouter une automatiquement et la nommera (par convention) `id`.
Elle sera ainsi accessible autant par cette propriété que par la propriété `pk`.

Chaque champ du modèle est donc typé et lié, soit à une primitive, soit à une autre instance au travers de sa clé d'identification.

Grâce à toutes ces informations, nous sommes en mesure de représenter facilement des livres liés à des catégories:

[source,python]
----
class Category(models.Model):
    name = models.CharField(max_length=255)

class Book(models.Model):
    title = models.CharField(max_length=255)
    category = models.ForeignKey(Category, on_delete=models.CASCADE)
----

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 (`name`), 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.

image::diagrams/books-foreign-keys-example.drawio.png[]

A présent que notre structure dispose de sa modélisation, il nous faut informer le moteur de base de données de créer la structure correspondance:

[source,shell]
----
$ python manage.py makemigrations
Migrations for 'library':
  library/migrations/0001_initial.py
    - Create model Category
    - Create model Book
----

Cette étape créera un fichier différentiel, explicitant les modifications à appliquer à la structure de données pour rester en corrélation avec la modélisation de notre application.

Nous pouvons écrire un premier code d'initialisation de la manière suivante: 

[source,python]
----
from library.models import Book, Category

movies = Category.objects.create(name="Adaptations au cinéma")
medieval = Category.objects.create(name="Médiéval-Fantastique")
science_fiction = Category.objects.create(name="Sciences-fiction")
computers = Category.objects.create(name="Sciences Informatiques")

books = {
    "Harry Potter": movies,
    "The Great Gatsby": movies,
    "Dune": science_fiction,
    "H2G2": science_fiction,
    "Ender's Game": science_fiction,
    "Le seigneur des anneaux": medieval,
    "L'Assassin Royal", medieval,
    "Clean code": computers,
    "Designing Data-Intensive Applications": computers
}

for book_title, category in books.items:
    Book.objects.create(name=book_title, category=category)
----

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_. Ces deux titres (au moins) peuvent appartenir à deux catégories distinctes.
* Pour _The Great Gatsby_, c'est l'inverse: nous l'avons initialement classé comme film, mais le livre existe depuis 1925.
* Nous pourrions sans doute également étoffer notre bibliothèque avec une catégorie spéciale "Baguettes magiques et trucs phalliques", à laquelle nous pourrons associer la saga _Harry Potter_ et ses dérivés.

En clair, notre modèle n'est pas adapté, et nous devons le modifier pour qu'une occurrence puisse être liée à plusieurs catégories. 
Au lieu d'utiliser un champ de type `ForeignKey`, nous utiliserons un champ de type `ManyToMany`, c'est-à-dire qu'à présent, un livre pourra être lié à plusieurs catégories, et qu'inversément, une même catégorie pourra être liée à plusieurs livres.

[source,python,highlight=6]
----
class Category(models.Model):
    name = models.CharField(max_length=255)

class Book(models.Model):
    title = models.CharField(max_length=255)
    category = models.ManyManyField(Category, on_delete=models.CASCADE)
----

Notre code d'initialisation reste par contre identique: Django s'occupe parfaitement de gérer la transition.

==== Accès aux relations

[source,python]
----
# wish/models.py

class Wishlist(models.Model):
    pass


class Item(models.Model):
    wishlist = models.ForeignKey(Wishlist)
----

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.


[source,python]
----
# wish/models.py

class Wishlist(models.Model):
    pass


class Item(models.Model):
    wishlist = models.ForeignKey(Wishlist, related_name='items')
----

NOTE: Si, dans une classe A, plusieurs relations sont liées à une classe B, Django ne saura pas à quoi correspondra la relation inverse.
Pour palier à ce problème, nous fixons une valeur à l'attribut `related_name`.
Par facilité (et par conventions), prenez l'habitude de toujours ajouter cet attribut: votre modèle gagnera en cohérence et en lisibilité.
Si cette relation inverse n'est pas nécessaire, il est possible de l'indiquer (par convention) au travers de l'attribut `related_name="+"`.

A partir de maintenant, nous pouvons accéder à nos propriétés de la manière suivante:

[source,python]
----
# python manage.py shell

>>> from wish.models import Wishlist, Item
>>> wishlist = Wishlist.create('Liste de test', 'description')
>>> item = Item.create('Element de test', 'description', w)
>>>
>>> item.wishlist
<Wishlist: Wishlist object>
>>>
>>> wishlist.items.all()
[<Item: Item object>]
----

==== N+1 Queries


=== Unicité


=== Indices


==== Conclusions

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.

==== Metamodèle et introspection

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.

[source,python,highlight=5]
----
class Wish(models.Model):
    name = models.CharField(max_length=255)

    class Meta:
        ordering = ('name',) <1>
----
<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.

Pour sélectionner un objet au pif : `return Category.objects.order_by("?").first()`

Les propriétés de la classe Meta les plus utiles sont les suivates:

* `ordering` pour spécifier un ordre de récupération spécifique.
* `verbose_name` pour indiquer le nom à utiliser au singulier pour définir votre classe
* `verbose_name_plural`, pour le pluriel.
* `contraints` (Voir https://girlthatlovestocode.com/django-model[ici]-), par exemple

[source,python]
----
    constraints = [ # constraints added
        models.CheckConstraint(check=models.Q(year_born__lte=datetime.date.today().year-18), name='will_be_of_age'),
    ]
----

==== Choix

Voir https://girlthatlovestocode.com/django-model[ici]

[source,python]
----
class Runner(models.Model):

    # this is new:
    class Zone(models.IntegerChoices):
        ZONE_1 = 1, 'Less than 3.10'
        ZONE_2 = 2, 'Less than 3.25'
        ZONE_3 = 3, 'Less than 3.45'
        ZONE_4 = 4, 'Less than 4 hours'
        ZONE_5 = 5, 'More than 4 hours'

    name = models.CharField(max_length=50)
    last_name = models.CharField(max_length=50)
    email = models.EmailField()
    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
----

==== Validateurs





==== Constructeurs

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:

[source,python]
----
class Wishlist(models.Model):

    @staticmethod
    def create(name, description):
        w = Wishlist()
        w.name = name
        w.description = description
        w.save()
        return w

class Item(models.Model):

    @staticmethod
    def create(name, description, wishlist):
        i = Item()
        i.name = name
        i.description = description
        i.wishlist = wishlist
        i.save()
        return i
----

Mieux encore: on pourrait passer par un `ModelManager` pour limiter le couplage;
l'accès à une information stockée en base de données ne se ferait dès lors qu'au travers
de cette instance et pas directement au travers du modèle. De cette manière, on limite le
couplage des classes et on centralise l'accès.

[source,python]
----
class ItemManager(...):
    (de mémoire, je ne sais plus exactement :-))
----

=== Conclusion

Le modèle proposé par Django est un composant extrêmement performant, mais fort couplé avec le coeur du framework.
Si tous les composants peuvent être échangés avec quelques manipulations, le cas du modèle sera plus difficile à interchanger.

A côté de cela, il permet énormément de choses, et vous fera gagner un temps précieux, tant en rapidité d'essais/erreurs, que de preuves de concept.

Une possibilité peut également être de cantonner Django à un framework

