gwift-book/source/part-3-django-concepts/models.adoc

179 lines
7.5 KiB
Plaintext
Raw Normal View History

2020-04-11 21:28:52 +02:00
== Modélisation
2021-04-12 20:08:16 +02:00
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.
2020-04-11 21:28:52 +02:00
2021-04-06 19:29:02 +02:00
Django utilise un paradigme de type https://fr.wikipedia.org/wiki/Mapping_objet-relationnel[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, ...
2020-04-11 21:28:52 +02:00
2021-04-06 19:29:02 +02:00
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.
2021-04-05 20:53:29 +02:00
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*.
2020-04-11 21:28:52 +02:00
Assez de blabla, on démarre !
2021-04-05 20:53:29 +02:00
=== Modélisation de base
2020-04-11 21:28:52 +02:00
2021-04-05 20:53:29 +02:00
==== Types de champs
2020-04-11 21:28:52 +02:00
2021-04-05 20:53:29 +02:00
==== Clés étrangères et relations
2020-04-11 21:28:52 +02:00
. ForeignKey
. ManyToManyField
. OneToOneField
2020-04-11 21:28:52 +02:00
2021-04-06 19:29:02 +02:00
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.
2021-04-05 20:53:29 +02:00
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.
2021-04-06 19:29:02 +02:00
Dans notre modèle ci-dessus, nous n'avons jusqu'à présent eu besoin que des relations 1-N:
2021-04-06 19:29:02 +02:00
. La première entre les listes de souhaits et les souhaits;
2021-04-05 20:53:29 +02:00
. La seconde entre les souhaits et les parts.
[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')
----
2021-04-05 20:53:29 +02:00
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é.
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
2021-04-05 20:53:29 +02:00
>>> wishlist = Wishlist.create('Liste de test', 'description')
>>> item = Item.create('Element de test', 'description', w)
>>>
2021-04-05 20:53:29 +02:00
>>> item.wishlist
<Wishlist: Wishlist object>
>>>
2021-04-05 20:53:29 +02:00
>>> wishlist.items.all()
[<Item: Item object>]
----
2020-04-11 21:28:52 +02:00
2021-04-05 20:53:29 +02:00
==== 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 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()`, ... Bref, toutes les méthodes qui font qu'une instance est sait **comment** interagir avec la base de données. La base d'un https://en.wikipedia.org/wiki/Object-relational_mapping[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`.
2020-04-11 21:28:52 +02:00
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]
----
class Wish(models.Model):
name = models.CharField(max_length=255)
class Meta:
ordering = ('name',) <1>
----
<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ù 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.
2021-04-06 19:29:02 +02:00
* `verbose_name` pour indiquer le nom à utiliser au singulier pour définir votre classe
* `verbose_name_plural`, pour le pluriel.
2021-05-18 20:13:10 +02:00
* `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
----
2021-04-05 20:53:29 +02:00
==== Shell
==== 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 :-))
----