13 KiB
Modélisation
Nous allons aborder la modélisation des objets en elle-même, qui est en lien direct avec la conception de la base de données et la manière dont celles-ci s’agencent et communiquent entre elles.
Django utilise un paradigme de type ORM - c’est-à-dire que chaque type d’objet peut s’apparenter à une table SQL, mais en ajoutant une couche propre au modèle orienté objet. Il est ainsi possible de définir facilement des notions d’héritage (tout en restant dans une forme d’héritage simple), la possibilité d’utiliser des propriétés spécifiques, des classes intermédiaires, …
L’avantage de tout ceci est que tout reste au niveau du code. Si l’on revient sur la méthodologie des douze facteurs, ce point concerne principalement la minimisation de la divergence entre les environnements d’exécution. Déployer une nouvelle instance de l’application pourra être réalisé directement à partir d’une seule et même commande, dans la mesure où tout est embarqué au niveau du code.
Assez de blabla, on démarre !
Modélisation de base
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
.
Types de champs
Clés étrangères et relations
-
ForeignKey
-
ManyToManyField
-
OneToOneField
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. Les champs de type OneToOneField, pour représenter une relation 1-1.
Dans notre modèle ci-dessus, nous n’avons jusqu’à présent eu besoin que des relations 1-N:
-
La première entre les listes de souhaits et les souhaits;
-
La seconde entre les souhaits et les parts.
# 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.
# 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 pas 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:
# 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>]
Metamodèle
Quand on prend une classe (par exemple, Wishlist
que l’on a défini ci-dessus), on voit qu’elle hérite par défaut de models.Model
. On peut regarder les propriétés définies dans cette classe en analysant le fichier lib\site-packages\django\models\base.py
. On y voit notamment que models.Model
hérite de ModelBase
au travers de six pour la rétrocompatibilité vers Python 2.7.
Cet héritage apporte notamment les fonctions save()
, clean()
, delete()
, … Bref, toutes les méthodes qui font qu’une instance est sait comment interagir avec la base de données. La base d’un ORM, en fait.
D’autre part, 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.
class Wish(models.Model):
name = models.CharField(max_length=255)
class Meta:
ordering = ('name',) (1)
-
On définit 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ù untop 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 ici-), par exemple
constraints = [ # constraints added
models.CheckConstraint(check=models.Q(year_born__lte=datetime.date.today().year-18), name='will_be_of_age'),
]
Choix
Voir ici
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
Protocoles de langage (dunder methods)
The Python data model specifies a lot of specially named methods that can be overridden in your custom classes to provide them with additional syntax capabilities. You can recognize these methods by their specific naming conventions that wrap the method name with double underscores. Because of this, they are sometimes referred to as dunder methods. It is simply shorthand for double underscores.The most common and obvious example of such dunder methods is init(), which is used for class instance initialization:
class CustomUserClass:
def __init__(self, initiatization_argument):
...
En fait, l’intérêt concerne surtout la représentation de nos modèles, puisque chaque classe du modèle est représentée par la définition d’un objet Python. Nous pouvons donc utiliser ces mêmes dunder methods (double-underscores methods) pour étoffer les protocoles du langage.
These methods, either alone or when defined in a specific combination, constitute the so-called language protocols. If we say that an object implements a specific language protocol, it means that it is compatible with a specific part of the Python language syntax. The following is a table of the most common protocols within the Python language.Protocol nameMethodsDescriptionCallable protocolcall()Allows objects to be called with parentheses:instance()Descriptor protocolsset(), get(), and del()Allows us to manipulate the attribute access pattern of classes (see the Descriptors section)Container protocolcontains()Allows us to test whether or not an object contains some value using the in keyword:value in instance Python in Comparison with Other LanguagesIterable protocoliter()Allows objects to be iterated using the forkeyword:for value in instance: …Sequence protocolgetitem(),len()Allows objects to be indexed with square bracket syntax and queried for length using a built-in function:item = instance[index]length = len(instance)Each operator available in Python has its own protocol and operator overloading happens by implementing the dunder methods of that protocol. Python provides over 50 overloadable operators that can be divided into five main groups:• Arithmetic operators • In-place assignment operators• Comparison operators• Identity operators• Bitwise operatorsThat’s a lot of protocols so we won’t discuss all of them here. We will instead take a look at a practical example that will allow you to better understand how to implement operator overloading on your own
A full list of available dunder methods can be found in the Data model section of the official Python documentation available at https://docs.python.org/3/reference/datamodel.html. All operators are also exposed as ordinary functions in the operators module. The documentation of that module gives a good overview of Python operators. It can be found at https://docs.python.org/3.9/library/operator.html
The add()
method is responsible for overloading the +
(plus sign) operator and here it allows us to add
two matrices together.
Only matrices of the same dimensions can be added together.
This is a fairly simple operation that involves adding all matrix elements one by one to form a new matrix.
The sub()
method is responsible for overloading the –
(minus sign) operator that will be responsible for matrix subtraction.
To subtract two matrices, we use a similar technique as in the – operator:
def __sub__(self, other):
if (len(self.rows) != len(other.rows) or len(self.rows[0]) != len(other.rows[0])):
raise ValueError("Matrix dimensions don't match")
return Matrix([[a - b for a, b in zip(a_row, b_row)] for a_row, b_row in zip(self.rows, other.rows) ])
And the following is the last method we add to our class:
def __mul__(self, other):
if not isinstance(other, Matrix):
raise TypeError(f"Don't know how to multiply {type(other)} with Matrix")
if len(self.rows[0]) != len(other.rows):
raise ValueError("Matrix dimensions don't match")
rows = [[0 for _ in other.rows[0]] for _ in self.rows]
for i in range(len (self.rows)):
for j in range(len (other.rows[0])):
for k in range(len (other.rows)):
rows[i][j] += self.rows[i][k] * other.rows[k][j]
return Matrix(rows)
The last overloaded operator is the most complex one.
This is the *
operator, which is implemented through the mul()
method.
In linear algebra, matrices don’t have the same multiplication operation as real numbers.
Two matrices can be multiplied if the first matrix has a number of columns equal to the number of rows of the second matrix.
The result of that operation is a new matrix where each element is a dot product of the corresponding row of the first matrix
and the corresponding column of the second matrix.
Here we’ve built our own implementation of the matrix to present the idea of operators overloading.
Although Python lacks a built-in type for matrices, you don’t need to build them from scratch.
The NumPy package is one of the best Python mathematical packages and among others provides native support for matrix algebra.
You can easily obtain the NumPy package from PyPI
(Voir Expert Python Programming, 4th Edition, page 142-144)
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:
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.
class ItemManager(...):
(de mémoire, je ne sais plus exactement :-))