Notions avancées sur les modèles
Clés naturelles
Pour visualiser le code relatif à ce chapitre : git diff v1.6.1..v1.7.0.
1. Utilité des clés naturelles
Une clé naturelle est un attribut ou un ensemble d’attributs d’un modèle permettant d’identifier de manière unique une instance. Ainsi, lorsque le nom d’un jeu est unique, sa connaissance suffit à déterminer précisément de quel jeu il s’agit.
Lorsqu’un nom d’utilisateur est connu, il est également possible d’identifier précisément le joueur concerné. Ce n’est pas nécessairement le cas avec un prénom et un nom, puisqu’il peut exister des homonymes.
L’objectif est donc d’utiliser des clés naturelles pour désigner les objets plutôt que leurs clés primaires. Cette approche facilite notamment l’export et la réimportation de données entre différentes bases, les clés primaires pouvant différer d’un environnement à l’autre et entraîner des risques d’écrasement de données.
2. Convertir les modèles en clés naturelles
Pour cela, il faut créer une méthode natural_key dans le modèle.
Voyons un exemple pour le modèle du joueur :
class Player(models.Model):
...
def natural_key(self) -> tuple[str]:
return (self.user.username,)
Cette méthode doit renvoyer un N-uplet. Ici, comme indiqué précédemment, connaître le nom d’utilisateur du joueur suffit à identifier le joueur. Il est généralement pertinent de typer statiquement ces méthodes.
Notons que l’utilisateur est un champ OneToOne, donc qu’il y a une unicité sur ce champ.
Voyons ce qu’il en est pour le jeu :
class Game(models.Model):
...
def natural_key(self) -> tuple[str]:
return (self.name,)
Le même principe s’applique : connaître le nom d’un jeu suffit à savoir de quel jeu il s’agit. Ce champ possède également une contrainte d’unicité...
Mixins
Pour visualiser le code relatif à ce chapitre : git diff v1.7.0..v1.7.1.
1. Notions autour de l’héritage
La première est l’héritage multitable, qui correspond à un véritable mécanisme d’héritage. Un modèle parent est défini, puis des modèles fils en héritent. Les données du modèle parent sont stockées dans la table parente, tandis que les données propres à chaque modèle fils sont stockées dans leur table respective. Chaque modèle fils possède en outre une relation un à un vers le modèle parent.
Il y a ensuite les classes abstraites. Dans ce cas, il s’agit d’une notion d’héritage Python appliquée aux classes de modèles. L’idée est simple : hériter d’une classe revient ici à recopier ses champs dans sa propre classe. C’est une technique à utiliser si vous avez des champs qui se retrouvent dans plusieurs modèles et que vous souhaitez éviter le copier-coller. C’est ce que nous allons voir dans cette section.
Enfin, il y a la notion de modèles proxy. L’idée derrière ces classes est assez différente, puisqu’un modèle proxy permet de mettre des outils fonctionnels différents derrière les mêmes données...
Migration liée aux mixins
1. Modification d’une migration
Pour visualiser le code relatif à cette section : git diff v1.7.0..v1.7.1, comme pour la section précédente.
Nous étions en train de finaliser un nouveau Mixin et nous allons devoir gérer la migration pour qu’elle renomme ces champs au lieu de les supprimer pour en ajouter de nouveaux, nous évitant ainsi de perdre les données.
Il ne faut donc pas lancer la commande suivante :
$ make update
Car elle fait la migration et l’applique immédiatement. Au lieu de cela, il faut plutôt ouvrir un terminal et faire les choses manuellement :
$ make bash
$ poetry run python manage.py makemigrations
On verra que pour chaque champ created_at, il nous demande une valeur par défaut, car elle n’est pas précisée dans le modèle. On prendra la suggestion donnée par le programme en tapant 1 pour saisir cette valeur et juste entrée pour valider la valeur par défaut.
À noter qu’il est possible, si on le souhaite, de taper une autre valeur, voire de saisir la suggestion timezone.now. Il ne faut toutefois surtout pas taper timezone.now(), auquel cas une date est créée à cet instant précis, puis écrite en dur, c’est-à-dire enregistrée comme date de création de la migration.
On devrait voir ce résultat-là :
Migrations for 'app':
app/migrations/0008_remove_player_creation_datetime_and_more.py
- Remove field creation_datetime from player
- Remove field last_modification_datetime from player
+ Add field created_at to answer
+ Add field updated_at to answer
+ Add field created_at to game
+ Add field updated_at to game
+ Add field created_at to play
+ Add field updated_at to play
+ Add field created_at to player
+ Add field updated_at to player
+ Add field created_at to question
+ Add field updated_at to question
On voit en gras les quatre opérations que l’on ne veut...
Signaux
Pour visualiser le code relatif à cette section : git diff v1.7.3..v1.7.4.
1. Explications générales sur les signaux
Les signaux sont un moyen de communiquer qu’il se passe quelque chose. Par exemple :
-
Un objet a été créé en base de données.
-
Un objet a été modifié en base de données.
-
Un objet a été supprimé de la base de données.
-
Une migration est sur le point de commencer.
-
Une migration vient de se terminer.
-
Une requête vient d’arriver.
-
Une requête vient d’être traitée.
Ces signaux sont envoyés par Django. Ils permettent de s’y connecter afin d’exécuter du code.
Par exemple, il est possible de dire :
-
à la création d’un jeu, calculer automatiquement le slug ;
-
à la création d’un jeu, créer automatiquement un jeu de questions et réponses basique ;
-
à la saisie d’une réponse, s’assurer que le nombre de points de la question liée est cohérent avec celui des réponses ;
-
à la suppression d’une réponse, s’assurer que le nombre de points de la question est cohérent avec les réponses qui restent.
Ces exemples seront implémentés dans cette section.
Ces signaux sont appelés en permanence. Ils constituent donc un moyen efficace d’écrire du code exécuté quelle que soit la manière dont l’objet est modifié.
Quelques paramètres génériques doivent être présentés :
-
sender : quel est l’origine du signal (dans nos exemples, le modèle de l’instance) ;
-
instance : l’objet courant, sur lequel le signal est en train de s’appliquer ;
-
using : nom de la base de données concernée (utile lorsque l’application en utilise plusieurs) ;
-
raw : Vrai si on est en train d’injecter des données, comme c’est le cas avec make load.
Pour terminer, il est également possible de créer des signaux personnalisés.
Par contre, il est aussi possible d’utiliser des signaux pour communiquer avec d’autres applications. Dans ce cas, nous utiliserons un broker de données comme RabbitMQ, Kafka ou encore Redis...
Modèles récursifs
Pour visualiser le code relatif à cette section : git diff v1.7.4..v1.7.5.
1. Qu’est-ce qu’un modèle récursif ?
Il s’agit simplement d’un modèle qui pointe vers lui-même. L’un de ses champs est une clé étrangère vers le même modèle, ce qui permet de créer un arbre de données.
Nous avons déjà écrit un tel modèle, le voici :
class Category(Model):
objects = CategoryManager()
parent = ForeignKey(
verbose_name=gettext("parent"),
related_name="child_set",
to="self",
null=True,
blank=True,
db_index=True,
on_delete=CASCADE,
)
label = CharField(
verbose_name=gettext("label"),
max_length=127,
blank=False,
db_index=True,
unique=True,
)
@property
def depth(self):
if self.parent_id:
return self.parent.depth + 1
return 0
@property
def full_label(self):
if self.parent_id:
return self.parent.full_label + " > " + self.label
return self.label
@property
def...Polymorphisme
Pour visualiser le code relatif à cette section : git diff v1.7.5..v1.7.6.
1. Installer le module django-polymorphic
Pour commencer, ce nouveau module doit être installé.
Pour cela, il existe deux options.
Pour la première, il faut démarrer une console, puis taper la commande :
$ make bash
$ poetry add django-polymorphic
Cette commande va modifier le fichier pyproject.toml ainsi que le fichier poetry.lock.
La seconde méthode consiste à rajouter directement la ligne suivante dans pyproject.toml :
django-polymorphic = "^4.1.0"
Puis, il faut utiliser la commande suivante pour générer un fichier poetry.lock cohérent avec ce dernier :
$ make lock
Une fois ceci fait, il faut rajouter, dans le fichier project/settings.py, la ligne suivante :
INSTALLED_APPS = [
...
'polymorphic',
...
]
2. Modèles polymorphiques
Pour visualiser précisément les changements, la commande suivante peut être utilisée sur le projet fil rouge cloné :
$ git diff v1.7.4..v1.7.5 project/app/models.py
Dans cette section, nous n’allons pas créer un nouveau modèle, mais modifier un modèle existant, ce qui revêt une complexité plus importante dans le sens où il faudra gérer les migrations.
Un modèle polymorphique hérite de PolymorphicModel au lieu de Model. Une classe mère est déclarée, puis des classes filles. Dans cet exemple, la classe Player est transformée en classe mère.
Avant, nous avions cela :
class Player(TrackingMixin, models.Model):
...
Maintenant, nous avons cela :
from polymorphic.models import PolymorphicModel
class Player(TrackingMixin, PolymorphicModel):
...
Une fois ce changement effectué, de nouvelles classes peuvent être créées pour distinguer les différents types de joueurs.
Voici une première classe :
class Guest(Player):
"""Player invited by another player."""
invited_by = models.ForeignKey(
verbose_name=gettext("invited...