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

10 KiB
Raw Blame History

Modélisation

On va aborder la modélisation des objets en elle-même, qui sapparente à la conception de la base de données.

Django utilise un modèle ORM - cest-à-dire que chaque objet peut sapparenter à une table SQL, mais en ajoutant une couche propre au paradigme orienté objet. Il sera ainsi possible de définir facilement des notions dhéritage (tout en restant dans une forme dhéritage simple), la possibilité dutiliser des propriétés spécifiques, des classes intermédiaires, …​

Lavantage de tout ceci est que tout reste au niveau du code. Si lon revient sur la méthodologie des douze facteurs, ce point concerne principalement la minimisation de la divergence entre les environnements dexécution. Déployer une nouvelle instance de lapplication pourra être réalisé directement à partir dune seule et même commande, dans la mesure où tout est embarqué au niveau du code.

Assez de blabla, on démarre !

Types de champs

Clés étrangères et relations

  1. ForeignKey

  2. ManyToManyField

  3. OneToOneField

Dans les examples ci-dessus, nous avons vu les relations multiples (1-N), représentées par des ForeignKey dune classe A vers une classe B. 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 navons 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 linstance 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 lattribut 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')

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

# python manage.py shell

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

Remarque: 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 et pour gagner en cohérence, on fixe alors une valeur à lattribut related_name.

Querysets et managers

NOTE : faudra sortir les queryset du chapitre...

LORM de Django (et donc, chacune des classes qui composent votre modèle) propose par défaut deux objets hyper importants:

  • Les managers, qui consistent en un point dentrée pour accéder aux objets persistants

  • Les querysets, qui permettent de filtrer des ensembles ou sous-ensemble dobjets. Les querysets peuvent simbriquer, pour ajouter dautres filtres à des filtres existants.

Ces deux propriétés vont de paire; par défaut, chaque classe de votre modèle propose un attribut objects, qui correspond à un manager (ou un gestionnaire, si vous préférez). Ce gestionnaire constitue linterface par laquelle vous accéderez à la base de données. Mais pour cela, vous aurez aussi besoin dappliquer certains requêtes ou filtres. Et pour cela, vous aurez besoin des querysets, qui consistent en des …​ ensembles de requêtes :-).

Si on veut connaître la requête SQL sous-jacente à lexécution du queryset, il suffit dappeler la fonction str() sur la propriété query:

queryset = Wishlist.objects.all()

print(queryset.query)

Conditions AND et OR sur un queryset

Pour un `AND`, il suffit de chaîner les conditions. ** trouver un exemple ici ** :-)
Mais en gros : bidule.objects.filter(condition1, condition2)
Il existe deux autres options : combiner deux querysets avec l'opérateur `&` ou combiner des Q objects avec ce même opérateur.

Soit encore combiner des filtres:

from core.models import Wish

Wish.objects (1)
Wish.objects.filter(name__icontains="test").filter(name__icontains="too") (2)
  1. Ca, cest notre manager.

  2. 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".

Pour un 'OR', on a deux options :

  1. Soit passer par deux querysets, typiuqment queryset1 | queryset2

  2. Soit passer par des Q objects, que lon trouve dans le namespace django.db.models.

from django.db.models import Q

condition1 = Q(...)
condition2 = Q(...)

bidule.objects.filter(condition1 | condition2)

Lopérateur inverse (NOT)

Idem que ci-dessus : soit on utilise la méthode exclude sur le queryset, soit lopérateur ~ sur un Q object;

Ajouter les sujets suivants :

  1. Prefetch

  2. select_related

Metamodèle

Quand on prend une classe (par exemple, Wishlist que lon a défini ci-dessus), on voit quelle 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 quune instance est sait comment interagir avec la base de données. La base dun ORM, en fait.

Dautre part, chaque classe héritant de models.Model possède une propriété objects. Comme on la vu dans la section Jouons un peu avec la console, cette propriété permet daccéder aux objects persistants dans la base de données, au travers dun 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 lensemble des requêtes effectuées sur cette classe.

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 quil ne sera pas possible de modifier cet ordre (la méthode order_by existe et peut être chaînée à nimporte quel queryset). Doù linté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.

Migrations

Les migrations (comprendre les "migrations du schéma de base de données") sont intimement liées à la représentation dun contexte fonctionnel. Lajout dune nouvelle information, dun nouveau champ ou dune nouvelle fonction peut saccompagner de tables de données à mettre à jour ou de champs à étendre.

Toujours dans une optique de centralisation, les migrations sont directement embarquées au niveau du code. Le développeur soccupe de créer les migrations en fonction des actions à entreprendre; ces migrations peuvent être retravaillées, squashées, …​ et feront partie intégrante du processus de mise à jour de lapplication.

A noter que les migrations nappliqueront de modifications que si le schéma est impacté. Ajouter une propriété related_name sur une ForeignKey nengendrera aucune nouvelle action de migration, puisque ce type daction ne sapplique que sur lORM, et pas directement sur la base de données: au niveau des tables, rien ne change. Seul le code et le modèle sont impactés.

En gros, soit on supprime toutes les migrations (en conservant le fichier __init__.py), soit on réinitialise proprement les migrations avec un --fake-initial (sous réserve que toutes les personnes qui utilisent déjà le projet s'y conforment... Ce qui n'est pas gagné.

Shell

Les validateurs

A retenir

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; laccès à une information stockée en base de données ne se ferait dès lors quau 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 laccès.

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