Überprüfen Sie, ob OneToOneField in Django None ist

86

Ich habe zwei Modelle wie dieses:

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


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

Ich muss etwas tun, wenn der Benutzer ein Profil vom Typ 1 oder Typ 2 hat:

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

Für Benutzer, die weder Typ1- noch Typ2-Profile haben, führt die Ausführung eines solchen Codes jedoch zu folgendem Fehler:

Type1Profile matching query does not exist.

Wie kann ich den Profiltyp eines Benutzers überprüfen?

Vielen Dank

John Bright
quelle

Antworten:

92

Mit der folgenden hasattrFunktion können Sie überprüfen, ob die Beziehung (OneToOne) vorhanden ist oder nicht :

if hasattr(request.user, 'type1profile'):
    # do something
elif hasattr(request.user, 'type2profile'):
    # do something else
else:
    # do something else
Joctee
quelle
3
Vielen Dank für diese Lösung. Leider funktioniert das nicht immer. Wenn Sie select_related()jetzt oder in Zukunft damit arbeiten möchten - oder vielleicht sogar sicher sein möchten, dass Sie auch mit anderen Arten von Magie umgehen, die anderswo auftreten können - müssen Sie den Test wie folgt erweitern:if hasattr(object, 'onetoonerevrelattr') and object.onetoonerevrelattr != None
Klassenstapler
7
Beachten Sie, dass in Python <3.2 alle Ausnahmen hasattrverschluckt werden , die während der Datenbanksuche auftreten, und nicht nur . Dies ist wahrscheinlich kaputt und nicht das, was Sie wollen. DoesNotExist
Pi Delport
funktioniert nicht mit Python 2.7. Auch wenn OneToOne nicht vorhanden ist, wird ein Objekt django.db.models.fields.related.RelatedManager zurückgegeben.
Alexpirine
@alartur Welche Django-Version benutzt du?
Joctee
Django 1.5. Aber ich habe mein spezielles Problem gelöst, indem ich das, was ich tun wollte, auf eine ganz andere Art und Weise umgesetzt habe.
Alexpirine
48

Sie können feststellen, ob eine nullfähige Eins-zu-Eins-Beziehung für ein bestimmtes Modell null ist, indem Sie einfach das entsprechende Feld im Modell auf NoneNess testen , aber nur, wenn Sie das Modell testen, aus dem die Eins-zu-Eins-Beziehung stammt. Zum Beispiel angesichts dieser beiden Klassen…

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()

… Um zu sehen, ob a ein Restauranthat Place, können wir den folgenden Code verwenden:

>>> 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!

Um zu sehen, ob a eine Placehat Restaurant, ist es wichtig zu verstehen, dass das Referenzieren der restaurantEigenschaft auf eine Instanz von Placeeine Restaurant.DoesNotExistAusnahme auslöst, wenn es kein entsprechendes Restaurant gibt. Dies geschieht, weil Django intern eine Suche durchführt QuerySet.get(). Beispielsweise:

>>> 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.

In diesem Szenario herrscht Occams Rasiermesser vor, und der beste Ansatz, um festzustellen, ob a ein Placehat oder nicht, Restautrantwäre ein Standard try/ exceptKonstrukt, wie hier beschrieben .

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

Während der Vorschlag von joctee, hasattrin der Praxis zu arbeiten, tatsächlich nur zufällig funktioniert, werden alle Ausnahmen (einschließlich ) im Gegensatz zu nur s hasattrunterdrückt , wie es sollte. Wie Pi Delport betonte, wurde dieses Verhalten in Python 3.2 gemäß dem folgenden Ticket tatsächlich korrigiert: http://bugs.python.org/issue9666 . Außerdem - und auf die Gefahr hin opinionated - ich glaube , das oben / Konstrukt ist repräsentativ dafür , wie Django funktioniert, während der Verwendung kann das Problem für Neulinge Wolke, die FUD und Verbreitung schlechten Gewohnheiten können erstellen.DoesNotExistAttributeErrortryexcepthasattr

BEARBEITEN Don Kirkbys vernünftiger Kompromiss erscheint mir ebenfalls vernünftig.

Joshua Pokotilow
quelle
19

Ich mag Joctees Antwort , weil es so einfach ist.

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

Andere Kommentatoren haben Bedenken geäußert, dass es mit bestimmten Versionen von Python oder Django möglicherweise nicht funktioniert, aber die Django-Dokumentation zeigt diese Technik als eine der Optionen:

Sie können auch hasattr verwenden, um zu vermeiden, dass Ausnahmen abgefangen werden müssen:

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

Natürlich zeigt die Dokumentation auch die Ausnahmefangtechnik:

p2 hat kein zugeordnetes Restaurant:

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

Ich stimme Joshua zu, dass das Fangen der Ausnahme klarer macht, was passiert, aber es scheint mir nur chaotischer. Vielleicht ist das ein vernünftiger Kompromiss?

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

Dies ist nur die Abfrage der RestaurantObjekte nach Ort. Es kehrt zurück, Nonewenn dieser Ort kein Restaurant hat.

Hier ist ein ausführbares Snippet, mit dem Sie mit den Optionen spielen können. Wenn Sie Python, Django und SQLite3 installiert haben, sollte es nur ausgeführt werden. Ich habe es mit Python 2.7, Python 3.4, Django 1.9.2 und SQLite3 3.8.2 getestet.

# 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
quelle
10

Wie wäre es mit Try / Except-Blöcken?

def get_profile_or_none(user, profile_cls):

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

    return profile

Dann verwenden Sie so!

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!

Ich nehme an, Sie könnten dies als generische Funktion verwenden, um eine umgekehrte OneToOne-Instanz zu erhalten, wenn eine Ursprungsklasse (hier: Ihre Profilklassen) und eine verwandte Instanz (hier: request.user) angegeben sind.

Geradeausanwalt
quelle
3

Verwenden Sie select_related!

>>> user = User.objects.select_related('type1profile').get(pk=111)
>>> user.type1profile
None
ivan133
quelle
2
Ich weiß, dass es so funktioniert, aber ist dieses Verhalten von select_related tatsächlich dokumentiert?
Kos
3
Ich habe es gerade in Django 1.9.2 versucht und es wird ausgelöst RelatedObjectDoesNotExist.
Don Kirkby
1

falls Sie das Modell haben

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

Und Sie müssen nur für jeden Benutzer wissen, ob UserProfile vorhanden ist oder nicht - der aus Sicht der Datenbank effizienteste Weg , eine vorhandene Abfrage zu verwenden .

Die vorhandene Abfrage gibt nur einen booleschen Wert zurück und nicht den umgekehrten Attributzugriff wie hasattr(request.user, 'type1profile')dies. Dadurch wird eine Abfrage generiert und eine vollständige Objektdarstellung zurückgegeben

Dazu müssen Sie dem Benutzermodell eine Eigenschaft hinzufügen

class User(AbstractBaseUser)

@property
def has_profile():
    return UserProfile.objects.filter(user=self.pk).exists()
Pymen
quelle
0

Ich verwende eine Kombination aus has_attr und ist 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
quelle
0

Einer der intelligenten Ansätze wird hinzuzufügen benutzerdefiniertes Feld OneToOneOrNoneField und verwendet es [Werke für 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)

Implementierung

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

Verwendung

r = Restaurant(serves_hot_dogs=True, serves_pizza=False)
r.place  # will return None
Pymen
quelle
Django 1.8 müssen Sie verwenden , SingleRelatedObjectDescriptoranstatt ReverseOneToOneDescriptorwie diese from django.db.models.fields.related import SingleRelatedObjectDescriptor
pymen