Django: récupère un objet de la base de données, ou 'None' si rien ne correspond

101

Existe-t-il une fonction Django qui me permettra d'obtenir un objet de la base de données, ou None si rien ne correspond?

En ce moment, j'utilise quelque chose comme:

foo = Foo.objects.filter(bar=baz)
foo = len(foo) > 0 and foo.get() or None

Mais ce n'est pas très clair et c'est compliqué d'en avoir partout.

David Wolever
la source
2
Vous savez que vous pouvez simplement utiliser foo = foo [0] if foo else None
Visgean Skeloru
2
Python a un opérateur ternaire, vous n'avez pas besoin d'utiliser d'opérateurs booléens. De plus, len(foo)c'est mauvais : " Remarque: n'utilisez pas len () sur les QuerySets si tout ce que vous voulez faire est de déterminer le nombre d'enregistrements dans l'ensemble. Il est beaucoup plus efficace de gérer un décompte au niveau de la base de données, en utilisant SELECT COUNT de SQL (), et Django fournit une méthode count () précisément pour cette raison. ". Réécrit:foo = foo[0] if foo.exists() else None
mustafa.0x
1
@ mustafa.0x Je ne pense pas que cela s'applique ici. Ici, la vérification du nombre exécuterait une autre requête. Ensuite, nous aurions à nouveau une requête pour obtenir les données réelles. C'est un cas où le filtrage, puis le comptage auraient plus de sens ... mais pas plus de sens que le filtrage et l'appel first(): P
MicronXD

Réponses:

88

Dans Django 1.6, vous pouvez utiliser la first()méthode Queryset. Il renvoie le premier objet correspondant à l'ensemble de requêtes, ou None s'il n'y a pas d'objet correspondant.

Usage:

p = Article.objects.order_by('title', 'pub_date').first()
Cesar Canassa
la source
19
Ce n'est pas une bonne réponse, si plusieurs objets sont trouvés, alors MultipleObjectsReturned()n'est pas déclenché, comme documenté ici . Vous pouvez trouver de meilleures réponses ici
sleepycal
25
@sleepycal Je ne suis pas d'accord. Le first()fonctionne exactement comme il le suppose. Il renvoie le premier objet dans le résultat de la requête et ne se soucie pas si plusieurs résultats sont trouvés. Si vous avez besoin de vérifier plusieurs objets renvoyés, utilisez .get()plutôt.
Cesar Canassa
7
[édité] Comme le PO l'a dit, .get()ne convient pas à ses besoins. La réponse correcte à cette question peut être trouvée ci-dessous par @kaapstorm, et est clairement la réponse la plus appropriée. Abuser de filter()cette façon peut entraîner des conséquences inattendues, ce que OP n'a probablement pas réalisé (sauf si je manque quelque chose ici)
sleepycal
1
@sleepycal Quelles conséquences involontaires découlent de cette approche?
HorseloverFat
12
Si vos contraintes de base de données ne gèrent pas correctement l'unicité entre les objets, vous pouvez alors atteindre des cas extrêmes où votre logique s'attend à ce qu'il n'y ait qu'un seul objet (qui est sélectionné par first ()), mais en réalité plusieurs objets existent. Utiliser get () au lieu de first () vous donne une couche de protection supplémentaire, en augmentant MultipleObjectsReturned(). Si le résultat renvoyé ne doit pas renvoyer plusieurs objets, il ne doit pas être traité comme tel. Il y a eu un long débat à ce sujet ici
sleepycal
139

Il y a deux façons de faire ça;

try:
    foo = Foo.objects.get(bar=baz)
except model.DoesNotExist:
    foo = None

Ou vous pouvez utiliser un wrapper:

def get_or_none(model, *args, **kwargs):
    try:
        return model.objects.get(*args, **kwargs)
    except model.DoesNotExist:
        return None

Appelez ça comme ça

foo = get_or_none(Foo, baz=bar)
Nick Craig-Wood
la source
8
Je n'aime pas le fait qu'il utilise des exceptions pour la fonctionnalité - c'est une mauvaise pratique dans la plupart des langues. Comment est-ce en python?
pcv
18
C'est ainsi que fonctionne l'itération Python (déclenche une StopIteration), et c'est une partie essentielle du langage. Je ne suis pas non plus un grand fan de la fonctionnalité try / except, mais elle semble être considérée comme pythonique.
Matt Luongo
4
@pcv: il n'y a pas de sagesse conventionnelle fiable sur "la plupart" des langues. Je suppose que vous pensez à un langage spécifique que vous utilisez, mais soyez prudent avec de tels quantificateurs. Les exceptions peuvent être aussi lentes (ou «assez rapides» d'ailleurs) que n'importe quoi en Python. Revenons à la question - c'est une lacune du framework, qu'ils n'ont fourni aucune querysetméthode pour le faire avant la 1.6! Mais quant à cette construction, elle est tout simplement maladroite. Ce n'est pas "mal" parce qu'un auteur de tutoriel de votre langue préférée l'a dit. Essayez google: "demandez pardon plutôt que par permission".
Tomasz Gandor
@TomaszGandor: Ouais, c'est maladroit et difficile à lire, quoi qu'en dise votre tutoriel de langue préféré. Quand je vois une exception, je suppose qu'il se passe quelque chose de non standard. La lisibilité compte. Mais je suis d'accord quand il n'y a pas d'autre option raisonnable, vous devriez y aller et rien de mal ne se passera.
pcv
@pcv La getméthode n'est pas censée retourner None, donc une exception a du sens. Vous êtes censé l'utiliser dans les cas où vous êtes sûr que l'objet sera là / l'objet est "supposé" y être. Utiliser également des exceptions comme celle-ci est considéré comme «correct» car, eh bien, cela a du sens. Vous voulez un getavec un contrat différent, donc vous attrapez l'exception et la supprimez, ce qui est le changement que vous recherchez.
Dan Passaro
83

Pour ajouter un exemple de code à la réponse de sorki (j'ajouterais ceci en commentaire, mais c'est mon premier article, et je n'ai pas assez de réputation pour laisser des commentaires), j'ai implémenté un gestionnaire personnalisé get_or_none comme ceci:

from django.db import models

class GetOrNoneManager(models.Manager):
    """Adds get_or_none method to objects
    """
    def get_or_none(self, **kwargs):
        try:
            return self.get(**kwargs)
        except self.model.DoesNotExist:
            return None

class Person(models.Model):
    name = models.CharField(max_length=255)
    objects = GetOrNoneManager()

Et maintenant je peux faire ceci:

bob_or_none = Person.objects.get_or_none(name='Bob')
kaapstorm
la source
C'est très élégant.
NotSimon
13

Vous pouvez également essayer d'utiliser django ennuyeux (il a une autre fonction utile!)

installez-le avec:

pip install django-annoying

from annoying.functions import get_object_or_None
get_object_or_None(Foo, bar=baz)
Llazzaro
la source
9

Donnez à Foo son gestionnaire personnalisé . C'est assez simple: mettez simplement votre code en fonction dans le gestionnaire personnalisé, définissez le gestionnaire personnalisé dans votre modèle et appelez-le avec Foo.objects.your_new_func(...).

Si vous avez besoin d'une fonction générique (pour l'utiliser sur n'importe quel modèle, pas seulement avec un gestionnaire personnalisé), écrivez la vôtre et placez-la quelque part sur votre chemin python et importez-la, pas plus compliquée.

sorki
la source
4

Que ce soit via un gestionnaire ou une fonction générique, vous voudrez peut-être aussi attraper 'MultipleObjectsReturned' dans l'instruction TRY, car la fonction get () lèvera cela si vos kwargs récupèrent plus d'un objet.

S'appuyant sur la fonction générique:

def get_unique_or_none(model, *args, **kwargs):
    try:
        return model.objects.get(*args, **kwargs)
    except (model.DoesNotExist, model.MultipleObjectsReturned), err:
        return None

et dans le gestionnaire:

class GetUniqueOrNoneManager(models.Manager):
    """Adds get_unique_or_none method to objects
    """
    def get_unique_or_none(self, *args, **kwargs):
        try:
            return self.get(*args, **kwargs)
        except (self.model.DoesNotExist, self.model.MultipleObjectsReturned), err:
            return None
poudre émulsionnée
la source
Cela semble saisir tous les meilleurs points des autres réponses. Nice one :)
Edward Newell
@emispowder, que faire si je ne veux pas renvoyer None, je veux renvoyer un message d'alerte comme "aucune donnée correspondante" s'affichant sur la MÊME page?
Héléna
0

Voici une variante de la fonction d'assistance qui vous permet de passer éventuellement une QuerySetinstance, au cas où vous voudriez obtenir l'objet unique (le cas échéant) à partir d'un ensemble de requêtes autre que l'ensemble de allrequêtes d'objets du modèle (par exemple, à partir d'un sous-ensemble d'éléments enfants appartenant à un instance parent):

def get_unique_or_none(model, queryset=None, *args, **kwargs):
    """
        Performs the query on the specified `queryset`
        (defaulting to the `all` queryset of the `model`'s default manager)
        and returns the unique object matching the given
        keyword arguments.  Returns `None` if no match is found.
        Throws a `model.MultipleObjectsReturned` exception
        if more than one match is found.
    """
    if queryset is None:
        queryset = model.objects.all()
    try:
        return queryset.get(*args, **kwargs)
    except model.DoesNotExist:
        return None

Cela peut être utilisé de deux manières, par exemple:

  1. obj = get_unique_or_none(Model, *args, **kwargs) comme discuté précédemment
  2. obj = get_unique_or_none(Model, parent.children, *args, **kwargs)
Gary
la source
-1

Je pense que dans la plupart des cas, vous pouvez simplement utiliser:

foo, created = Foo.objects.get_or_create(bar=baz)

Seulement s'il n'est pas critique qu'une nouvelle entrée soit ajoutée dans la table Foo (les autres colonnes auront les valeurs None / par défaut)

TomerP
la source