Tutos Django

Utilisation avancée de l'ORM de Django

Une fois que vous avez créé vos modèles, Django vous fournit une API d'abstraction de base de données vous permettant de créer, retrouver, mettre à jour ou supprimer vos objets.
Nous allons voir ensemble les principales méthodes de cette API.

Considérons ces modèles :

class Blog(models.Model):
    name = models.CharField(max_length=100)
    tagline = models.TextField()

    def __unicode__(self):
        return self.name

class Author(models.Model):
    name = models.CharField(max_length=50)
    email = models.EmailField()

    def __unicode__(self):
        return self.name

class Entry(models.Model):
    blog = models.ForeignKey(Blog)
    headline = models.CharField(max_length=255)
    body_text = models.TextField()
    pub_date = models.DateTimeField()
    mod_date = models.DateTimeField()
    authors = models.ManyToManyField(Author)
    n_comments = models.IntegerField()
    n_pingbacks = models.IntegerField()
    rating = models.IntegerField()

    def __unicode__(self):
        return self.headline

Vous remarquerez que nous utilisons dans ces modèles une nouvelle notion, le 'ManyToMany' field qui agit comme une ForeignKey mais qui permet de spécifier plusieurs relations. Plus d'informations sur le ManyToManyField peuvent être trouvées sur la documentation officielle : ManyToManyField

Pour créer un objet, c'est simple, il nous suffit de l'instancier et de le sauvegarder en base de données à l'aide de la méthode save.

>>> from blog.models import Blog
>>> b = Blog(name='Beatles Blog', tagline='All the latest Beatles news.')
>>> b.save()

Cela équivaut à faire un INSERT dans la base de données en langage SQL. Gardez toutes fois à l'esprit que l'action impactant sur la base de données ne se fera qu'avec la méthode save. C'est pourquoi si vous voulez mettre à jour un objet déjà créé et déjà sauvegardé, il vous faudra rappeler cette méthode :

>>> b.name = 'New name'
>>> b.save()

Sauvegarder un champ définit par une ForeignKey se fait de la même façon, tant que vous passez les bons types :

>>> from blog.models import Entry
>>> entry = Entry.objects.get(pk=1)
>>> cheese_blog = Blog.objects.get(name="Cheddar Talk")
>>> entry.blog = cheese_blog
>>> entry.save()

Le champ ManyToManyField fonctionne un peu différemment car la méthode 'add' doit être utilisée étant donné que plusieurs paramètres peuvent être passés :

>>> from blog.models import Author
>>> joe = Author.objects.create(name="Joe")
>>> entry.authors.add(joe)
>>> john = Author.objects.create(name="John")
>>> paul = Author.objects.create(name="Paul")
>>> george = Author.objects.create(name="George")
>>> ringo = Author.objects.create(name="Ringo")
>>> entry.authors.add(john, paul, george, ringo)

Maintenant que nous savons créer des objets et les sauvegarder en base de données, passons à la récupération de ces objets.
L'API Django fourmille d'outils de récupération pour vous simplifier la vie.

Pour récupérer tous les objets d'un certain type, nous utiliserons 'all' :

>>> all_entries = Entry.objects.all()

Il vous est également possible d'utiliser la philosophie du slice sur les objets à récupérer :

>>> Entry.objects.all()[:5]

Cela récupèrera les 5 premiers objets Entry dans la base.

Avec des filtres !

>>> Entry.objects.filter(pub_date__year=2006)

Nous utilisons ici le méthode 'filter' afin de récupérer les objets dont la date de publication correspond à l'année 2006.
En effet, le champ pub_date est un DateTimeField : équivalent à un objet DateTime ! Et cet objet possède un attribut year.
Il est donc possible de faire directement des requêtes sur les attributs d'un objet au moyen du double underscore : '__'

Il est même possible de chaîner vos filtres et d'exclure certaines données :

>>> Entry.objects.filter(
...     headline__startswith='What'
... ).exclude(
...     pub_date__gte=datetime.now()
... ).filter(
...     pub_date__gte=datetime(2005, 1, 1)
... )

Ici nous récupérons tous les objets Entry dont l'attribut headline commence par "What" avec une date de publication plus petite que la date d'aujourd'hui et plus grande que le 1° Janvier 2005.
Le mot clef exclude permet donc d'exclure certains résulats.
Vous remarquerez aussi l'utilisation de nouveaux filtres comme startswith et gte qui ont été appelés via un double underscore '__' de la même manière que l'on récupère l'attribut d'un objet en utilisant son getter. Ces filtres sont en réalité des Field lookups que nous allons lister plus tard dans ce tutoriel.

Enfin, si l'on désire récupérer un objet unique, il est possible d'utiliser la méthode get :

>>> one_entry = Entry.objects.get(pk=1)

le mot clef pk signifie ici primary key, et cela vous permet de récupérer les objets en passant en paramètre à la fonction 'get' l'id de l'objet que vous voulez récupérer.
Mais attention ! Si aucun objet avec la clef spécifiée existe, django lèvera une exception du type : Entry.DoesNotExist et si plusieurs objets sont trouvés l'exception MultipleObjectsReturned sera levée. Pensez donc à bien entourer de try/except vos bouts de code utilisant la méthode get. Par ailleurs, vous pouvez utilisez n'importe quel paramètre dans la fonction get combiné à un field lookup.
En réalité utiliser le lookup pk, revient simplement à utiliser id combiné au field lookup exact :

>>> Blog.objects.get(id__exact=14) # Explicit form
>>> Blog.objects.get(id=14) # __exact is implied
>>> Blog.objects.get(pk=14) # pk implies id__exact

Par défaut, le lookup exact est utilisé, mais il est possible d'utiliser par exemple le lookup iexact qui lui, permet de faire des recherches non sensibles à la casse, exemple :

>>> Blog.objects.get(name__iexact="beatles blog")

Cela récupèrera un objet de type Blog dont le nom peut être : "Beatles Blog", "beatles blog", ou même "BeAtlES blOG".

Autre lookup intéressant 'contains' :

>>> Entry.objects.get(headline__contains='Lennon')

qui va récupérer les Entry dont l'attribut headline contient Lennon. Attention ici, la casse est sensitive, si vous vouliez récupérer aussi les objets dont le headline contient 'lennon' par exemple, vous auriez du utiliser icontains.

La liste des Field Lookups est importante et nous en avons vu ici les plus utilisés.
Vous pouvez obtenir la liste complète illustrée par des exemples sur la documentation officielle : Field lookups
De même, la liste des méthodes de récupération ne se réduit pas uniquement à all, filter, exclude et get. Toutes les méthodes sont disponibles sur : Methods that return new QuerySets (Jetez un oeil aux très intéressantes méthodes values et values_list ;))

Petite astuce, quand vous faites des requêtes sur des objets, vous pouvez passer au paramètre du filtre, un objet instancié ou la clef primaire de l'objet :

>>> Entry.objects.filter(blog=b) # Query using object instance
>>> Entry.objects.filter(blog=b.id) # Query using id from instance
>>> Entry.objects.filter(blog=5) # Query using id directly

Imaginez maintenant que vous désirez comparer le champ d'un modèle non plus avec une constante mais avec un autre champ de ce même modèle !
Et bien cela est possible grâce à la fonction F :

>>> from django.db.models import F
>>> Entry.objects.filter(n_comments__gt=F('n_pingbacks'))

F supporte l'addition, la soustraction, la multiplication, la division, le modulo arithmétique et même les lookups.
Vous pouvez très bien écrire :

>>> from datetime import timedelta
>>> Entry.objects.filter(mod_date__gt=F('pub_date') + timedelta(days=3))

ou

>>> Entry.objects.filter(authors__name=F('blog__name'))

Nous savons comment faire de la récupération des données en utilisant des filtres qui fusionnent des données et en excluent mais quand est-il de la fonction 'OU' ?
Et bien la fonction Q est là pour nous donner la réponse !

Q(question__startswith='Who') | Q(question__startswith='What')

Cette expression permet de sélectionner l'objet dont les questions commencent par 'Who' ou 'What' !
On peut également mélanger les Q fonctions avec des lookups classiques :

from django.db.models import Q
Poll.objects.get(
    Q(pub_date=date(2005, 5, 2)) | Q(pub_date=date(2005, 5, 6)),
    question__startswith='Who')

Mais vous devez bien faire attention de spécifier les expressions Q en premier argument, car l'écriture de ce code n'est pas valide :

# INVALID QUERY
Poll.objects.get(
    question__startswith='Who',
    Q(pub_date=date(2005, 5, 2)) | Q(pub_date=date(2005, 5, 6)))

Plus d'informations sur la documentation officielle : Complex lookups with Q objects

Maintenant que nous savons correctement récupérer des données, voyons les autres fonctions fondamentales de l'API : 

create permet d'instancier un objet et de le sauvegarder automatiquement dans la base de données, grâce à cela, il ne vous est pas obligé d'appeler la méthode save, exemple :

>>> from blog.models import Author
>>> joe = Author.objects.create(name="Joe")

delete permet de supprimer des objets de la base de données :

>>> Entry.objects.filter(pub_date__year=2005).delete()

Cela va supprimer tous les objets Entry dont l'année de date de publication est égale à 2005.

b = Blog.objects.get(pk=1)
# This will delete the Blog and all of its Entry objects.
b.delete()

Appeler delete, effectue en réalité un 'ON DELETE CASCADE' en code SQL, ce qui signifie que tout ce qui est relaté à l'objet Blog sera effacé lui aussi.

update permet de modifier un ou plusieurs objets directement depuis la base :

# Update all the headlines with pub_date in 2007.
Entry.objects.filter(pub_date__year=2007).update(headline='Everything is the same')

add et remove permettent d'ajouter ou d'enlever des objets sur un autre objet :

>>> from blog.models import Author
>>> joe = Author.objects.create(name="Joe")
>>> paul = Author.objects.create(name="Paul")
>>> george = Author.objects.create(name="George")
>>> ringo = Author.objects.create(name="Ringo")
>>> entry.authors.add(john, paul, george, ringo)
>>> entry.authors.remove(john, paul)

clear enlève tous les objets relatés à un autre objet :

>>> from blog.models import Author
>>> joe = Author.objects.create(name="Joe")
>>> paul = Author.objects.create(name="Paul")
>>> george = Author.objects.create(name="George")
>>> ringo = Author.objects.create(name="Ringo")
>>> entry.authors.add(john, paul, george, ringo)
>>> entry.authors.clear()

Enfin, il est temps de parler de la notion d'agrégation. Considérons à présent ce modèle :

class Author(models.Model):
   name = models.CharField(max_length=100)
   age = models.IntegerField()
   friends = models.ManyToManyField('self', blank=True)

class Publisher(models.Model):
   name = models.CharField(max_length=300)
   num_awards = models.IntegerField()

class Book(models.Model):
   isbn = models.CharField(max_length=9)
   name = models.CharField(max_length=300)
   pages = models.IntegerField()
   price = models.DecimalField(max_digits=10, decimal_places=2)
   rating = models.FloatField()
   authors = models.ManyToManyField(Author)
   publisher = models.ForeignKey(Publisher)
   pubdate = models.DateField()

class Store(models.Model):
   name = models.CharField(max_length=300)
   books = models.ManyToManyField(Book)

L'agrégation va permettre de retourner des objets de type dictionnaire (clef/valeur) sur une opération commune donnée. Exemple :

>>> from django.db.models import Avg, Max, Min, Count
>>> Book.objects.aggregate(Avg('price'), Max('price'), Min('price'))
{'price__avg': 34.35, 'price__max': Decimal('81.20'), 'price__min': Decimal('12.99')}

Ici avec l'aide des mots clef aggregate et Avg, Max, Min ; on récupère dans un dictionnaire la moyenne de prix, le prix minimum et le prix maximum des objets de type Book.

Légèrement différent, le mot clef annotate va permettre de réaliser la même chose mais de retourner un objet Queryset disposant des attributs générés :

# Build an annotated queryset
>>> q = Book.objects.annotate(Count('authors'))
# Interrogate the first object in the queryset
>>> q[0]
<Book: The Definitive Guide to Django>
>>> q[0].authors__count
2
# Interrogate the second object in the queryset
>>> q[1]
<Book: Practical Django Projects>
>>> q[1].authors__count
1

Pour aller encore plus loin, il est possible de faire des opérations d'agrégation ou d'annotation sur les champs à relation tels que ManyToMany ou ForeignKey et d'utiliser des lookups

>>> Store.objects.annotate(min_price=Min('books__price'), max_price=Max('books__price'))
>>> Store.objects.aggregate(youngest_age=Min('books__authors__age'))

Plus d'informations sur les agrégations et les annotations sont disponibles sur la documentation officielle : Aggregation

Si vous êtes un nostalgique des requêtes SQL à la main, il est possible d'en réaliser grâce à la méthode extra ou grâce à raw : Performing raw SQL queries que nous ne détaillerons pas dans ce tutoriel consacré à l'ORM de Django.

Pour aller plus loin, voici quelques liens de la documentation officielle qui vous permettront d'approfondir les connaissances dévoilées ici : Making queries, QuerySet API reference, Database access optimization

Article suivant

Article précédent

Articles similaires

Commentaires

Les commentaires sont fermés.

Pingbacks

Les pingbacks sont fermés.