Complete models.tex based on easter notes
continuous-integration/drone/push Build is failing Details

This commit is contained in:
Fred Pauchet 2022-05-01 10:54:22 +02:00
parent 63f2cf23f4
commit c8f88779ff
1 changed files with 123 additions and 73 deletions

View File

@ -152,15 +152,17 @@ En résumé, toutes les méthodes qui font qu'une instance sait \textbf{comment}
\section{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.
Django, de son côté, ajoute une couche de typage statique exigé par le lien sous-jacent avec les moteurs de base de données relationnelles.
Dans ce domaine, 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) \texttt{id}.
Par défaut, et si aucune propriété ne dispose d'un attribut \texttt{primary\_key=True}, Django s'occupera d'ajouter un champ \texttt{id} grâce à son héritage de la classe \texttt{models.Model}.
Elle sera ainsi accessible autant par cette propriété que par la propriété \texttt{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.
Chaque champ du modèle est donc typé et lié, soit à un primitif, 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:
Grâce à toutes ces informations, nous sommes en mesure de représenter facilement des relations, par exemple des livres liés à des catégories:
\begin{minted}{python}
class Category(models.Model):
@ -171,13 +173,11 @@ class Book(models.Model):
category = models.ForeignKey(Category, on_delete=models.CASCADE)
\end{minted}
Par défaut, et si aucune propriété ne dispose d'un attribut \texttt{primary\_key=True}, Django s'occupera d'ajouter un champ \texttt{id} grâce à son héritage de la classe \texttt{models.Model}.
Les autres champs nous permettent d'identifier une catégorie (\texttt{Category}) par un nom (\texttt{name}), tandis qu'un livre (\texttt{Book}) le sera par ses propriétés \texttt{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.
\includegraphics{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:
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, grâce à la création d'une étape de migration:
\begin{verbatim}
$ python manage.py makemigrations
@ -224,11 +224,11 @@ Nous nous rendons rapidement compte qu'un livre peut appartenir à plusieurs cat
\item
Pour \emph{The Great Gatsby}, c'est l'inverse: nous l'avons initialement classé comme film, mais le livre existe depuis 1925.
\item
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 \emph{Harry Potter} et ses dérivés.
Nous pourrions sans doute également étoffer notre bibliothèque avec une catégorie supplémentaire "Baguettes magiques et trucs phalliques", à laquelle nous pourrons associer la saga \emph{Harry Potter} et ses dérivés.
\end{itemize}
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 \texttt{ForeignKey}, nous utiliserons un champ de type \texttt{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.
En clair, notre modèle n'est pas adapté, et nous devons le modifier pour qu'une occurrence d'un livre puisse être liée à plusieurs catégories.
Au lieu d'utiliser un champ de type \texttt{ForeignKey}, nous utiliserons à présent un champ de type \texttt{ManyToMany}, c'est-à-dire qu'un livre pourra être lié à plusieurs catégories, et qu'inversément, une même catégorie pourra être liée à plusieurs livres.
\begin{minted}{python}
class Category(models.Model):
@ -236,7 +236,7 @@ class Category(models.Model):
class Book(models.Model):
title = models.CharField(max_length=255)
category = models.ManyManyField(Category, on_delete=models.CASCADE)
category = models.ManyManyField(Category)
\end{minted}
Notre code d'initialisation reste par contre identique: Django s'occupe parfaitement de gérer la transition.
@ -314,13 +314,57 @@ class Runner(models.Model):
\section{Validateurs}
La validation des champs intervient sur toute donnée entrée via le modèle.
Cette validation dispose de trois niveaux:
\begin{enumerate}
\item Directement au niveau du modèle, au travers de validateurs sur les champs
\item Via une méthode de nettoyage sur un champ, qui permet de prendre d'autres informations contextuelles en considération
\item Via une méthode de nettoyage globale, associée à l'instance.
\end{enumerate}
\subsection{Validation d'un champ}
La première manière de valider le contenu d'un champ est aussi la plus simple.
En prenant un modèle type:
\begin{minted}{python}
from django.db import models
def validate_title(value):
if 't' not in value:
raise ValidationError('Title does not start with T')
class Book(models.Model):
title = models.CharField(max_length=255, validators=[validate_title])
\end{minted}
\subsection{Clean\_<field\_name>}
Ca, c'est le deuxième niveau. Le contexte donne accès à déjà plus d'informations et permet de valider les informations en interogeant (par exemple) la base de données.
\begin{minted}{python}
class Bidule(models.Model):
def clean_title(self):
raise ValidationError('Title does not start with T')
\end{minted}
Il n'est plus nécessaire de définir d'attribut \texttt{validators=[...]}, puisque Django va appliquer un peu d'introspection pour récupérer toutes les méthodes qui commencent par \texttt{clean\_} et pour les faire correspondre au nom du champ à valider (ici, \texttt{title}).
\subsection{Clean}
Ici, c'est global: nous pouvons valider des données ou des champs globalement vis-à-vis d'autres champs déjà remplis.
\begin{minted}{python}
class Bidule(models.Model):
def clean(self):
raise ValidationError('Title does not start with T')
\end{minted}
\section{Constructeurs}
Si vous décidez de définir un constructeur sur votre modèle, ne surchargez pas la méthode \texttt{init}: créez plutôt une méthode static de type \texttt{create()}, en y associant les paramètres obligatoires ou
souhaités.
Mieux encore: on pourrait passer par un \texttt{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.
Si vous décidez de définir un constructeur sur votre modèle, ne surchargez pas la méthode \texttt{\_\_init\_\_}: créez plutôt une méthode static de type \texttt{create()}, en y associant les paramètres obligatoires ou souhaités.
Mieux encore: nous pouvons passer par un \texttt{ModelManager} pour limiter le couplage; l'accès à une information stockée en base de données ne se fait dès lors qu'au travers de cette instance et pas directement au travers du modèle.
\begin{minted}{python}
class ItemManager(...):
@ -380,26 +424,22 @@ Il existe deux autres options : combiner deux querysets avec l'opérateur `&` ou
Soit encore combiner des filtres:
\begin{minted}{python}
from core.models import Wish
from core.models import Wish
Wish.objects
Wish.objects.filter(name__icontains="test").filter(name__icontains="too")
Wish.objects
Wish.objects.filter(name__icontains="test").filter(name__icontains="too")
\end{minted}
\begin{itemize}
\item
Ca, c'est notre manager.
\item
Et là, on chaîne les requêtes pour composer une recherche sur tous les
souhaits dont le nom contient (avec une casse insensible) la chaîne
"test" et dont le nom contient la chaîne "too".
\end{itemize}
\item
Ca, c'est notre manager.
\item
Et là, on chaîne les requêtes pour composer une recherche sur tous les souhaits dont le nom contient (avec une casse insensible) la chaîne "test" et dont le nom contient la chaîne "too".
\end{itemize}
Pour un 'OR', on a deux options :
Pour un 'OR', on a deux options :
\begin{enumerate}
\def\labelenumi{\arabic{enumi}.}
\item
Soit passer par deux querysets, typiuqment
\texttt{queryset1\ \textbar{}\ queryset2}
@ -409,12 +449,12 @@ Wish.objects.filter(name__icontains="test").filter(name__icontains="too")
\end{enumerate}
\begin{minted}{python}
from django.db.models import Q
from django.db.models import Q
condition1 = Q(...)
condition2 = Q(...)
condition1 = Q(...)
condition2 = Q(...)
bidule.objects.filter(condition1 | condition2)
bidule.objects.filter(condition1 | condition2)
\end{minted}
L'opérateur inverse (\emph{NOT})
@ -422,50 +462,38 @@ L'opérateur inverse (\emph{NOT})
Idem que ci-dessus : soit on utilise la méthode \texttt{exclude} sur le
queryset, soit l'opérateur \texttt{\textasciitilde{}} sur un Q object;
\section{Optimisation}
\subsection{N+1 Queries}
\begin{itemize}
\item
\url{http://stackoverflow.com/questions/12681653/when-to-use-or-not-use-iterator-in-the-django-orm}
\item
\url{https://docs.djangoproject.com/en/1.9/ref/models/querysets/\#django.db.models.query.QuerySet.iterator}
\item
\url{http://blog.etianen.com/blog/2013/06/08/django-querysets/}
\end{itemize}
Deux solutions:
\begin{enumerate}
\def\labelenumi{\arabic{enumi}.}
\item
Prefetch
\item
select\_related
\end{enumerate}
\subsection{Unicité}
\end{enumerate}
\subsection{Indices}
\section{Agrégation et annotations}
Après analyse seulement.
\subsection{Agrégation et annotations}
\url{https://docs.djangoproject.com/en/3.1/topics/db/aggregation/}
\section{Métamodèle et introspection}
Comme chaque classe héritant de \texttt{models.Model} possède une propriété \texttt{objects}.
Comme on l'a vu dans la section \textbf{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 \texttt{ModelManager}.
En plus de cela, il faut bien tenir compte des propriétés \texttt{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.
\begin{minted}{python}
class Wish(models.Model):
name = models.CharField(max_length=255)
class Meta:
ordering = ('name',)
\end{minted}
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 \texttt{order\_by} existe et peut être chaînée à n'importe quel \emph{queryset}). D'où l'intérêt de tester ce type de comportement, dans la mesure où un \texttt{top\ 1} dans votre code pourrait être modifié simplement par cette petite information.
Pour sélectionner un objet au pif: \texttt{return\ Category.objects.order\_by("?").first()}
Cette propriété permet d'accéder aux objects persistants dans la base de données, au travers d'un \texttt{ModelManager}.
Les propriétés de la classe Meta les plus utiles sont les suivates:
@ -481,6 +509,39 @@ Les propriétés de la classe Meta les plus utiles sont les suivates:
\texttt{contraints} (Voir \href{https://girlthatlovestocode.com/django-model}{ici}-), par exemple
\end{itemize}
\subsection{Ordre par défaut}
\begin{minted}{python}
class Wish(models.Model):
name = models.CharField(max_length=255)
class Meta:
ordering = ('name',)
\end{minted}
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 \texttt{order\_by} existe et peut être chaînée à n'importe quel \emph{queryset}).
D'où l'intérêt de tester ce type de comportement, dans la mesure où un \texttt{top\ 1} dans votre code pourrait être modifié simplement par cette petite information.
Pour sélectionner un objet au pif: \texttt{return\ Category.objects.order\_by("?").first()}
En plus de cela, il faut bien tenir compte des propriétés \texttt{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.
Cela signifie que le \texttt{top 1} utilisé dans le code pourrait être impacté en cas de modification de cette propriété \texttt{ordering}, ce qui corrobore la nécessité de \emph{tester le code dans son ensemble}.
\subsection{Représentation textuelle}
verbose\_name et verbose\_name\_plural + lien avec
\subsection{Contraintes}
Les contraintes sont de plusieurs types:
\begin{enumerate}
\item Unicité
\item Composée
\end{enumerate}
\begin{minted}{python}
constraints = [ # constraints added
models.CheckConstraint(check=models.Q(year_born__lte=datetime.date
@ -490,15 +551,6 @@ constraints = [ # constraints added
\section{Querysets et managers}
\begin{itemize}
\item
\url{http://stackoverflow.com/questions/12681653/when-to-use-or-not-use-iterator-in-the-django-orm}
\item
\url{https://docs.djangoproject.com/en/1.9/ref/models/querysets/\#django.db.models.query.QuerySet.iterator}
\item
\url{http://blog.etianen.com/blog/2013/06/08/django-querysets/}
\end{itemize}
L'ORM de Django (et donc, chacune des classes qui composent votre modèle) propose par défaut deux objets hyper importants:
\begin{itemize}
@ -751,8 +803,6 @@ Nous pourrions ainsi définir les classes suivantes:
w.save()
\end{minted}
\section{Conclusions}
Le modèle proposé par Django est un composant extrêmement performant, mais fort couplé avec le coeur du framework.
@ -761,7 +811,7 @@ 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.
Dans les examples ci-dessus, nous avons vu les relations multiples (1-N), représentées par des clés étrangères (\textbf{ForeignKey}) d'une classe A vers une classe B.
Dans les exemples ci-dessus, nous avons vu les relations multiples (1-N), représentées par des clés étrangères (\textbf{ForeignKey}) d'une classe A vers une classe B.
Pour représenter d'autres types de relations, il existe également les champs de type \textbf{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 \textbf{OneToOneField}, pour représenter une relation 1-1.