Filtre Django ou obtenir pour un seul objet?


147

J'avais un débat à ce sujet avec certains collègues. Existe-t-il un moyen préféré de récupérer un objet dans Django lorsque vous n'en attendez qu'un?

Les deux moyens évidents sont:

try:
    obj = MyModel.objects.get(id=1)
except MyModel.DoesNotExist:
    # We have no object! Do something...
    pass

Et:

objs = MyModel.objects.filter(id=1)

if len(objs) == 1:
    obj = objs[0]
else:
    # We have no object! Do something...
    pass

La première méthode semble plus correcte sur le plan du comportement, mais utilise des exceptions dans le flux de contrôle qui peuvent introduire une surcharge. Le second est plus rond-point mais ne soulèvera jamais d'exception.

Y a-t-il des réflexions sur laquelle de celles-ci est préférable? Qu'est-ce qui est le plus efficace?

Réponses:


177

get()est fourni spécifiquement pour ce cas . Utilise le.

L'option 2 est presque précisément la manière dont la get()méthode est réellement implémentée dans Django, il ne devrait donc pas y avoir de différence de "performance" (et le fait que vous y réfléchissiez indique que vous violez l'une des règles cardinales de la programmation, à savoir essayer de optimiser le code avant même qu'il ne soit écrit et profilé - jusqu'à ce que vous ayez le code et que vous puissiez l'exécuter, vous ne savez pas comment il fonctionnera, et essayer d'optimiser avant cela est un chemin de douleur).


Tout est correct mais peut-être que plus d'informations devraient être ajoutées pour répondre? 1. Python encourage try / except (voir EAFP ), c'est pourquoi QS.get()c'est bien. 2. Les détails importent: est-ce que "n'attendre qu'un seul" signifie toujours 0-1 objets, ou il est possible d'avoir plus de 2 objets et ce cas devrait également être traité (dans ce cas len(objs)est une idée terrible)? 3. Ne présumez rien des frais généraux sans benchmark (je pense que dans ce cas, ce try/exceptsera plus rapide tant qu'au moins la moitié des appels retournent quelque chose)
imposeren

> à savoir essayer d'optimiser le code avant même qu'il ne soit écrit et profilé. C'est une remarque intéressante. J'ai toujours pensé que je devrais penser à la manière la plus optionnelle d'implémenter quelque chose avant de l'implémenter. Est-ce faux? Pouvez-vous élaborer sur ce point? Y a-t-il une ressource qui explique cela en détail?
Parth Sharma

Je suis surpris que personne ne l'ait mentionné en premier (). D'autres conseils semblent indiquer que c'est l'appel lancé pour ce scénario. stackoverflow.com/questions/5123839/…
NeilG

29

Vous pouvez installer un module appelé django-ennoying puis faire ceci:

from annoying.functions import get_object_or_None

obj = get_object_or_None(MyModel, id=1)

if not obj:
    #omg the object was not found do some error stuff

1
pourquoi est-ce ennuyeux d'avoir une telle méthode? me va bien!
Thomas le

17

1 est correct. En Python, une exception a une surcharge égale à un retour. Pour une preuve simplifiée, vous pouvez regarder ceci .

2 C'est ce que fait Django dans le backend. getappelle filteret lève une exception si aucun élément n'est trouvé ou si plus d'un objet est trouvé.


1
Ce test est assez injuste. Une grande partie de la surcharge liée à la levée d'une exception est la gestion de la trace de pile. Ce test avait une longueur de pile de 1, ce qui est bien inférieur à ce que vous trouveriez habituellement dans une application.
Rob Young

@Rob Young: Que voulez-vous dire? Où voyez-vous la gestion de la trace de pile dans le schéma typique «demander pardon plutôt que permission»? Le temps de traitement dépend de la distance parcourue par l'exception, pas de la profondeur de tout cela (lorsque nous n'écrivons pas en java et que nous n'appelons pas e.printStackTrace ()). Et le plus souvent (comme dans la recherche de dictionnaire) - l'exception est lancée juste en dessous du try.
Tomasz Gandor

12

Je suis un peu en retard à la fête, mais avec Django 1.6 il y a la first()méthode sur les ensembles de requêtes.

https://docs.djangoproject.com/en/dev/ref/models/querysets/#django.db.models.query.QuerySet.first


Renvoie le premier objet correspondant à l'ensemble de requêtes, ou None s'il n'y a pas d'objet correspondant. Si le QuerySet n'a aucun ordre défini, alors le jeu de requêtes est automatiquement trié par la clé primaire.

Exemple:

p = Article.objects.order_by('title', 'pub_date').first()
Note that first() is a convenience method, the following code sample is equivalent to the above example:

try:
    p = Article.objects.order_by('title', 'pub_date')[0]
except IndexError:
    p = None

Cela ne garantit pas que vous n'avez qu'un seul objet dans une requête
py_dude

8

Je ne peux parler d'aucune expérience de Django mais l'option n ° 1 indique clairement au système que vous demandez 1 objet, alors que la deuxième option ne le fait pas. Cela signifie que l'option n ° 1 pourrait plus facilement tirer parti des index de cache ou de base de données, en particulier lorsque l'attribut sur lequel vous filtrez n'est pas garanti d'être unique.

Aussi (encore une fois, en spéculant) la deuxième option peut avoir à créer une sorte de collection de résultats ou d'objet itérateur puisque l'appel de filter () pourrait normalement retourner de nombreuses lignes. Vous contourneriez cela avec get ().

Enfin, la première option est à la fois plus courte et omet la variable temporaire supplémentaire - seulement une différence mineure mais chaque petite aide.


Aucune expérience avec Django mais toujours sur place. Être explicite, concis et sûr par défaut, sont de bons principes quel que soit le langage ou le cadre.
nevelis

8

Pourquoi tout ça marche? Remplacez 4 lignes par 1 raccourci intégré. (Cela fait son propre essai / sauf.)

from django.shortcuts import get_object_or_404

obj = get_object_or_404(MyModel, id=1)

1
C'est génial quand c'est le comportement souhaité, mais parfois, vous voudrez peut-être créer l'objet manquant, ou l'extraction était une information facultative.
SingleNegationElimination

2
C'est ce qui Model.objects.get_or_create()est pour
boatcoder

7

Quelques informations supplémentaires sur les exceptions. S'ils ne sont pas élevés, ils ne coûtent presque rien. Ainsi, si vous savez que vous allez probablement avoir un résultat, utilisez l'exception, car en utilisant une expression conditionnelle, vous payez le coût de la vérification à chaque fois, quoi qu'il arrive. D'un autre côté, ils coûtent un peu plus cher qu'une expression conditionnelle lorsqu'ils sont augmentés, donc si vous vous attendez à ne pas avoir de résultat avec une certaine fréquence (par exemple, 30% du temps, si la mémoire est bonne), la vérification conditionnelle s'avère pour être un peu moins cher.

Mais c'est l'ORM de Django, et probablement l'aller-retour vers la base de données, ou même un résultat mis en cache, est susceptible de dominer les caractéristiques de performance, alors privilégiez la lisibilité, dans ce cas, puisque vous attendez exactement un résultat, utilisez get().


4

J'ai un peu joué avec ce problème et j'ai découvert que l'option 2 exécute deux requêtes SQL, ce qui pour une tâche aussi simple est excessif. Voir mon annotation:

objs = MyModel.objects.filter(id=1) # This does not execute any SQL
if len(objs) == 1: # This executes SELECT COUNT(*) FROM XXX WHERE filter
    obj = objs[0]  # This executes SELECT x, y, z, .. FROM XXX WHERE filter
else: 
    # we have no object!  do something
    pass

Une version équivalente qui exécute une seule requête est:

items = [item for item in MyModel.objects.filter(id=1)] # executes SELECT x, y, z FROM XXX WHERE filter
count = len(items) # Does not execute any query, items is a standard list.
if count == 0:
   return None
return items[0]

En passant à cette approche, j'ai pu réduire considérablement le nombre de requêtes exécutées par mon application.


1

Question intéressante, mais pour moi, l'option n ° 2 pue une optimisation prématurée. Je ne suis pas sûr de ce qui est le plus performant, mais l'option n ° 1 me semble certainement plus pythonique.


1

Je suggère un design différent.

Si vous souhaitez exécuter une fonction sur un résultat possible, vous pouvez dériver de QuerySet, comme ceci: http://djangosnippets.org/snippets/734/

Le résultat est plutôt génial, vous pourriez par exemple:

MyModel.objects.filter(id=1).yourFunction()

Ici, le filtre renvoie un jeu de requêtes vide ou un jeu de requêtes avec un seul élément. Vos fonctions de jeu de requêtes personnalisées sont également chaînables et réutilisables. Si vous souhaitez effectuer pour toutes vos entrées: MyModel.objects.all().yourFunction().

Ils sont également idéaux pour être utilisés comme actions dans l'interface d'administration:

def yourAction(self, request, queryset):
    queryset.yourFunction()

0

L'option 1 est plus élégante, mais assurez-vous d'utiliser try..except.

D'après ma propre expérience, je peux vous dire que parfois vous êtes sûr qu'il ne peut pas y avoir plus d'un objet correspondant dans la base de données, et pourtant il y en aura deux ... (sauf bien sûr lors de l'obtention de l'objet par sa clé primaire).


0

Désolé d'ajouter un autre point sur ce problème, mais j'utilise le paginateur django, et dans mon application d'administration de données, l'utilisateur est autorisé à choisir ce sur quoi interroger. Parfois, c'est l'identifiant d'un document, mais sinon c'est une requête générale renvoyant plus d'un objet, c'est-à-dire un Queryset.

Si l'utilisateur interroge l'identifiant, je peux exécuter:

Record.objects.get(pk=id)

qui jette une erreur dans le paginateur de django, car il s'agit d'un Record et non d'un Queryset of Records.

J'ai besoin de courir:

Record.objects.filter(pk=id)

Ce qui renvoie un Queryset contenant un élément. Ensuite, le paginateur fonctionne très bien.


Pour utiliser le paginateur - ou toute fonctionnalité qui attend un QuerySet - votre requête doit renvoyer un QuerySet. Ne basculez pas entre l'utilisation de .filter () et .get (), restez avec .filter () et fournissez le filtre "pk = id", comme vous l'avez déjà réalisé. C'est le modèle de ce cas d'utilisation.
Cornel Masson

0

.avoir()

Renvoie l'objet correspondant aux paramètres de recherche donnés, qui doivent être au format décrit dans Recherches de champ.

get () déclenche MultipleObjectsReturned si plusieurs objets ont été trouvés. L'exception MultipleObjectsReturned est un attribut de la classe de modèle.

get () lève une exception DoesNotExist si un objet n'a pas été trouvé pour les paramètres donnés. Cette exception est également un attribut de la classe de modèle.

.filtre()

Renvoie un nouveau QuerySet contenant des objets qui correspondent aux paramètres de recherche donnés.

Remarque

utilisez get () lorsque vous souhaitez obtenir un seul objet unique et filter () lorsque vous souhaitez obtenir tous les objets correspondant à vos paramètres de recherche.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.