265 votes

Le moyen le plus rapide d'obtenir le premier objet d'un queryset dans django ?

Souvent, je me retrouve à vouloir récupérer le premier objet d'un queryset dans Django, ou retourner None s'il n'y en a pas. Il existe de nombreuses façons de procéder qui fonctionnent toutes. Mais je me demande laquelle est la plus performante.

qs = MyModel.objects.filter(blah = blah)
if qs.count() > 0:
    return qs[0]
else:
    return None

Cela entraîne-t-il deux appels à la base de données ? Cela semble être du gaspillage. Est-ce que c'est plus rapide ?

qs = MyModel.objects.filter(blah = blah)
if len(qs) > 0:
    return qs[0]
else:
    return None

Une autre option serait :

qs = MyModel.objects.filter(blah = blah)
try:
    return qs[0]
except IndexError:
    return None

Cela génère un seul appel à la base de données, ce qui est bien. Mais elle nécessite la création d'un objet d'exception la plupart du temps, ce qui est très gourmand en mémoire lorsque tout ce dont vous avez besoin est un test if trivial.

Comment puis-je faire cela avec un seul appel à la base de données et sans faire tourner la mémoire avec des objets d'exception ?

27 votes

Règle générale : Si vous êtes soucieux de minimiser les allers-retours de la DB, n'utilisez pas len() sur les querysets, utilisez toujours .count() .

7 votes

"Si vous êtes préoccupé par la création d'une exception supplémentaire, alors vous vous trompez, car Python utilise des exceptions partout. Est-ce que vous avez réellement évalué que c'est gourmand en mémoire dans votre cas ?

1 votes

@Leopd Et si vous aviez réellement évalué la réponse d'une manière ou d'une autre (ou au moins les commentaires), vous sauriez que ce n'est pas plus rapide. En fait, ça peut être plus lent, parce que vous créez une liste supplémentaire juste pour la jeter. Et tout cela n'est rien comparé au coût de l'appel d'une fonction python ou de l'utilisation de l'ORM de Django en premier lieu ! Un seul appel à filter() représente beaucoup, beaucoup de choses, beaucoup de fois plus lent que de lever une exception (qui sera quand même levée, car c'est comme ça que le protocole des itérateurs fonctionne !)

443voto

cod3monk3y Points 1109

Utilisez les méthodes de commodité .first() et .last() :

MyModel.objects.filter(blah=blah).first()

Ils avalent tous deux l'exception qui en résulte et retournent None si le queryset ne retourne aucun objet.

Ceux-ci ont été ajoutés dans Django 1.6, qui était publié en novembre 2013 .

2 votes

Il ne fait pas les [:1], donc ce n'est pas aussi rapide (à moins que vous n'ayez besoin d'évaluer l'ensemble des querys de toute façon).

23 votes

Également, first() et last() appliquer une ORDER BY sur une requête. Cela rendra les résultats déterministes mais ralentira très probablement la requête.

0 votes

@janek37 il n'y a pas de différences dans les performances. Comme indiqué par cod3monk3y, c'est une méthode pratique et elle ne lit pas l'ensemble des requêtes.

162voto

stormlifter Points 1236

Vous pouvez utiliser découpage de tableaux :

Entry.objects.all()[:1].get()

Qui peut être utilisé avec .filter() :

Entry.objects.filter()[:1].get()

Vous ne voudriez pas d'abord le transformer en liste, car cela obligerait à appeler tous les enregistrements de la base de données. Faites simplement ce qui précède et vous n'obtiendrez que le premier. Vous pouvez même utiliser .order_by() pour vous assurer que vous obtenez le premier que vous voulez.

Veillez à ajouter le .get() ou bien vous obtiendrez un Jeu de requêtes et non un objet.

13 votes

Il faudrait toujours l'envelopper dans un try... sauf ObjectDoesNotExist, qui est comme la troisième option originale mais avec un découpage.

1 votes

Quel est l'intérêt de définir une LIMITE si vous appelez get() à la fin ? Laissez l'ORM et le compilateur SQL décider de ce qui est le mieux pour son backend (par exemple, sur Oracle Django émule LIMIT, donc cela va nuire au lieu d'aider).

0 votes

J'ai utilisé cette réponse sans la queue .get(). Si une liste est retournée, je renvoie alors le premier élément de la liste.

48voto

Ignacio Vazquez-Abrams Points 312628
r = list(qs[:1])
if r:
  return r[0]
return None

1 votes

Si vous activez le traçage, je suis sûr que vous verrez même cet ajout. LIMIT 1 à la requête, et je ne sais pas si vous pouvez faire mieux que cela. Cependant, en interne __nonzero__ sur QuerySet est mis en œuvre comme try: iter(self).next() except StopIteration: return false... pour qu'il n'échappe pas à l'exception.

0 votes

@Ben : QuerySet.__nonzero__() n'est jamais appelé puisque le QuerySet est converti en un list avant d'en vérifier la véracité. D'autres exceptions peuvent toutefois se produire.

0 votes

@Aron : Cela peut générer un StopIteration exception.

7voto

Nikolay Fominyh Points 2739

Si vous prévoyez d'obtenir le premier élément souvent - vous pouvez étendre QuerySet dans cette direction :

class FirstQuerySet(models.query.QuerySet):
    def first(self):
        return self[0]

class ManagerWithFirstQuery(models.Manager):
    def get_query_set(self):
        return FirstQuerySet(self.model)

Définissez le modèle comme ceci :

class MyModel(models.Model):
    objects = ManagerWithFirstQuery()

Et utilise-le comme ça :

 first_object = MyModel.objects.filter(x=100).first()

0 votes

Call objects = ManagerWithFirstQuery as objects = ManagerWithFirstQuery() - N'OUBLIEZ PAS LES PARENTHES - de toute façon, vous m'avez aidé donc +1

4voto

Nauman Tariq Points 11

Cela peut être comme ceci

obj = model.objects.filter(id=emp_id)[0]

ou

obj = model.objects.latest('id')

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X