Vérifier si OneToOneField est None dans Django

86

J'ai deux modèles comme celui-ci:

class Type1Profile(models.Model):
    user = models.OneToOneField(User, unique=True)
    ...


class Type2Profile(models.Model):
    user = models.OneToOneField(User, unique=True)
    ...

Je dois faire quelque chose si l'utilisateur a un profil Type1 ou Type2:

if request.user.type1profile != None:
    # do something
elif request.user.type2profile != None:
    # do something else
else:
    # do something else

Mais, pour les utilisateurs qui n'ont pas de profil type1 ou type2, l'exécution de code comme celui-ci produit l'erreur suivante:

Type1Profile matching query does not exist.

Comment puis-je vérifier le type de profil d'un utilisateur?

Merci

John Bright
la source

Réponses:

93

Pour vérifier si la relation (OneToOne) existe ou non, vous pouvez utiliser la hasattrfonction:

if hasattr(request.user, 'type1profile'):
    # do something
elif hasattr(request.user, 'type2profile'):
    # do something else
else:
    # do something else
joctee
la source
4
Merci pour cette solution. Malheureusement, cela ne fonctionne pas tout le temps. Dans le cas où vous voudriez travailler avec select_related()maintenant ou dans le futur - ou peut-être même pour être sûr de gérer également d'autres sortes de magie qui peuvent se produire ailleurs - vous devez étendre le test comme suit:if hasattr(object, 'onetoonerevrelattr') and object.onetoonerevrelattr != None
class stacker
7
Notez que dans Python <3.2, hasattravalera toutes les exceptions qui se produisent lors de la recherche dans la base de données, et pas seulement DoesNotExist. Ceci est probablement cassé, et pas ce que vous voulez.
Pi Delport
ne fonctionne pas avec python 2.7. Même si OneToOne n'existe pas, il renvoie un objet django.db.models.fields.related.RelatedManager.
alexpirine le
@alartur quelle version de django utilisez-vous?
joctee
Django 1.5. Mais j'ai résolu mon problème particulier en mettant en œuvre ce que je voulais faire d'une manière totalement différente.
alexpirine le
48

Il est possible de voir si une relation un-à-un nullable est nulle pour un modèle particulier en testant simplement le champ correspondant sur le modèle pour Noneness, mais seulement si vous testez sur le modèle d'où provient la relation un-à-un. Par exemple, étant donné ces deux classes…

class Place(models.Model):
    name = models.CharField(max_length=50)
    address = models.CharField(max_length=80)

class Restaurant(models.Model):  # The class where the one-to-one originates
    place = models.OneToOneField(Place, blank=True, null=True)
    serves_hot_dogs = models.BooleanField()
    serves_pizza = models.BooleanField()

… Pour voir si a Restauranta un Place, nous pouvons utiliser le code suivant:

>>> r = Restaurant(serves_hot_dogs=True, serves_pizza=False)
>>> r.save()
>>> if r.place is None:
>>>    print "Restaurant has no place!"
Restaurant has no place!

Pour voir si a Placea un Restaurant, il est important de comprendre que le référencement de la restaurantpropriété sur une instance de Placelève une Restaurant.DoesNotExistexception s'il n'y a pas de restaurant correspondant. Cela se produit car Django effectue une recherche en interne en utilisant QuerySet.get(). Par exemple:

>>> p2 = Place(name='Ace Hardware', address='1013 N. Ashland')
>>> p2.save()
>>> p2.restaurant
Traceback (most recent call last):
    ...
DoesNotExist: Restaurant matching query does not exist.

Dans ce scénario, le rasoir d'Occam prévaut, et la meilleure approche pour déterminer si un Placea ou non Restautrantserait un standard try/ une exceptconstruction comme décrit ici .

>>> try:
>>>     restaurant = p2.restaurant
>>> except Restaurant.DoesNotExist:
>>>     print "Place has no restaurant!"
>>> else:
>>>     # Do something with p2's restaurant here.

Bien que la suggestion de joctee d'utiliser hasattrfonctionne dans la pratique, cela ne fonctionne vraiment que par accident car hasattrsupprime toutes les exceptions (y compris DoesNotExist) par opposition à juste AttributeErrors, comme il se doit. Comme l'a souligné Pi Delport , ce comportement a en fait été corrigé dans Python 3.2 par le ticket suivant: http://bugs.python.org/issue9666 . De plus - et au risque de paraître opiniâtre - je pense que le try/ exceptconstruct ci - dessus est plus représentatif du fonctionnement de Django, tandis que l'utilisation hasattrpeut brouiller le problème pour les débutants, ce qui peut créer des FUD et propager de mauvaises habitudes.

EDIT Le compromis raisonnable de Don Kirkby me semble également raisonnable.

Joshua Pokotilow
la source
19

J'aime la réponse de joctee , parce que c'est si simple.

if hasattr(request.user, 'type1profile'):
    # do something
elif hasattr(request.user, 'type2profile'):
    # do something else
else:
    # do something else

D'autres commentateurs ont soulevé des inquiétudes quant au fait que cela pourrait ne pas fonctionner avec certaines versions de Python ou Django, mais la documentation de Django montre cette technique comme l'une des options:

Vous pouvez également utiliser hasattr pour éviter d'avoir à capturer des exceptions:

>>> hasattr(p2, 'restaurant')
False

Bien sûr, la documentation montre également la technique de capture d'exceptions:

p2 n'a pas de restaurant associé:

>>> from django.core.exceptions import ObjectDoesNotExist
>>> try:
>>>     p2.restaurant
>>> except ObjectDoesNotExist:
>>>     print("There is no restaurant here.")
There is no restaurant here.

Je suis d'accord avec Joshua pour dire que le fait d'attraper l'exception rend plus clair ce qui se passe, mais cela me semble juste plus compliqué. Peut-être s'agit-il d'un compromis raisonnable?

>>> print(Restaurant.objects.filter(place=p2).first())
None

Il s'agit simplement d'interroger les Restaurantobjets par endroit. Il revient Nonesi cet endroit n'a pas de restaurant.

Voici un extrait d'exécutable pour que vous puissiez jouer avec les options. Si vous avez installé Python, Django et SQLite3, il devrait simplement s'exécuter. Je l'ai testé avec Python 2.7, Python 3.4, Django 1.9.2 et SQLite3 3.8.2.

# Tested with Django 1.9.2
import sys

import django
from django.apps import apps
from django.apps.config import AppConfig
from django.conf import settings
from django.core.exceptions import ObjectDoesNotExist
from django.db import connections, models, DEFAULT_DB_ALIAS
from django.db.models.base import ModelBase

NAME = 'udjango'


def main():
    setup()

    class Place(models.Model):
        name = models.CharField(max_length=50)
        address = models.CharField(max_length=80)

        def __str__(self):              # __unicode__ on Python 2
            return "%s the place" % self.name

    class Restaurant(models.Model):
        place = models.OneToOneField(Place, primary_key=True)
        serves_hot_dogs = models.BooleanField(default=False)
        serves_pizza = models.BooleanField(default=False)

        def __str__(self):              # __unicode__ on Python 2
            return "%s the restaurant" % self.place.name

    class Waiter(models.Model):
        restaurant = models.ForeignKey(Restaurant)
        name = models.CharField(max_length=50)

        def __str__(self):              # __unicode__ on Python 2
            return "%s the waiter at %s" % (self.name, self.restaurant)

    syncdb(Place)
    syncdb(Restaurant)
    syncdb(Waiter)

    p1 = Place(name='Demon Dogs', address='944 W. Fullerton')
    p1.save()
    p2 = Place(name='Ace Hardware', address='1013 N. Ashland')
    p2.save()
    r = Restaurant(place=p1, serves_hot_dogs=True, serves_pizza=False)
    r.save()

    print(r.place)
    print(p1.restaurant)

    # Option 1: try/except
    try:
        print(p2.restaurant)
    except ObjectDoesNotExist:
        print("There is no restaurant here.")

    # Option 2: getattr and hasattr
    print(getattr(p2, 'restaurant', 'There is no restaurant attribute.'))
    if hasattr(p2, 'restaurant'):
        print('Restaurant found by hasattr().')
    else:
        print('Restaurant not found by hasattr().')

    # Option 3: a query
    print(Restaurant.objects.filter(place=p2).first())


def setup():
    DB_FILE = NAME + '.db'
    with open(DB_FILE, 'w'):
        pass  # wipe the database
    settings.configure(
        DEBUG=True,
        DATABASES={
            DEFAULT_DB_ALIAS: {
                'ENGINE': 'django.db.backends.sqlite3',
                'NAME': DB_FILE}},
        LOGGING={'version': 1,
                 'disable_existing_loggers': False,
                 'formatters': {
                    'debug': {
                        'format': '%(asctime)s[%(levelname)s]'
                                  '%(name)s.%(funcName)s(): %(message)s',
                        'datefmt': '%Y-%m-%d %H:%M:%S'}},
                 'handlers': {
                    'console': {
                        'level': 'DEBUG',
                        'class': 'logging.StreamHandler',
                        'formatter': 'debug'}},
                 'root': {
                    'handlers': ['console'],
                    'level': 'WARN'},
                 'loggers': {
                    "django.db": {"level": "WARN"}}})
    app_config = AppConfig(NAME, sys.modules['__main__'])
    apps.populate([app_config])
    django.setup()
    original_new_func = ModelBase.__new__

    @staticmethod
    def patched_new(cls, name, bases, attrs):
        if 'Meta' not in attrs:
            class Meta:
                app_label = NAME
            attrs['Meta'] = Meta
        return original_new_func(cls, name, bases, attrs)
    ModelBase.__new__ = patched_new


def syncdb(model):
    """ Standard syncdb expects models to be in reliable locations.

    Based on https://github.com/django/django/blob/1.9.3
    /django/core/management/commands/migrate.py#L285
    """
    connection = connections[DEFAULT_DB_ALIAS]
    with connection.schema_editor() as editor:
        editor.create_model(model)

main()
Don Kirkby
la source
10

Que diriez-vous d'utiliser des blocs try / except?

def get_profile_or_none(user, profile_cls):

    try:
        profile = getattr(user, profile_cls.__name__.lower())
    except profile_cls.DoesNotExist:
        profile = None

    return profile

Ensuite, utilisez comme ça!

u = request.user
if get_profile_or_none(u, Type1Profile) is not None:
    # do something
elif get_profile_or_none(u, Type2Profile) is not None:
    # do something else
else:
    # d'oh!

Je suppose que vous pouvez l'utiliser comme une fonction générique pour obtenir n'importe quelle instance OneToOne inversée, étant donné une classe d'origine (ici: vos classes de profil) et une instance associée (ici: request.user).

Geradeausanwalt
la source
3

Utilisez select_related!

>>> user = User.objects.select_related('type1profile').get(pk=111)
>>> user.type1profile
None
ivan133
la source
2
Je sais que cela fonctionne comme ça, mais ce comportement de select_related est-il réellement documenté?
Kos
3
Je viens d'essayer cela dans Django 1.9.2, et cela augmente RelatedObjectDoesNotExist.
Don Kirkby le
1

au cas où vous auriez le modèle

class UserProfile(models.Model):
    user = models.OneToOneField(User, unique=True)

Et vous avez juste besoin de savoir pour tout utilisateur que UserProfile existe / ou non - le moyen le plus efficace du point de vue de la base de données d'utiliser la requête existe .

La requête Exists renverra uniquement un booléen, plutôt qu'un accès inversé aux attributs comme hasattr(request.user, 'type1profile')- qui générera une requête get et retournera une représentation complète de l'objet

Pour ce faire, vous devez ajouter une propriété au modèle utilisateur

class User(AbstractBaseUser)

@property
def has_profile():
    return UserProfile.objects.filter(user=self.pk).exists()
pymen
la source
0

J'utilise une combinaison de has_attr et est None:

class DriverLocation(models.Model):
    driver = models.OneToOneField(Driver, related_name='location', on_delete=models.CASCADE)

class Driver(models.Model):
    pass

    @property
    def has_location(self):
        return not hasattr(self, "location") or self.location is None
FreeWorlder
la source
0

L'une des approches intelligentes sera d'ajouter le champ personnalisé OneToOneOrNoneField et de l' utiliser [fonctionne pour Django> = 1.9]

from django.db.models.fields.related_descriptors import ReverseOneToOneDescriptor
from django.core.exceptions import ObjectDoesNotExist
from django.db import models


class SingleRelatedObjectDescriptorReturnsNone(ReverseOneToOneDescriptor):
    def __get__(self, *args, **kwargs):
        try:
            return super().__get__(*args, **kwargs)
        except ObjectDoesNotExist:
            return None


class OneToOneOrNoneField(models.OneToOneField):
    """A OneToOneField that returns None if the related object doesn't exist"""
    related_accessor_class = SingleRelatedObjectDescriptorReturnsNone

    def __init__(self, *args, **kwargs):
        kwargs.setdefault('null', True)
        kwargs.setdefault('blank', True)
        super().__init__(*args, **kwargs)

la mise en oeuvre

class Restaurant(models.Model):  # The class where the one-to-one originates
    place = OneToOneOrNoneField(Place)
    serves_hot_dogs = models.BooleanField()
    serves_pizza = models.BooleanField()

Usage

r = Restaurant(serves_hot_dogs=True, serves_pizza=False)
r.place  # will return None
pymen
la source
pour django 1.8, vous devez utiliser SingleRelatedObjectDescriptorplutôt que ReverseOneToOneDescriptorcomme ça from django.db.models.fields.related import SingleRelatedObjectDescriptor
pymen