在 Django 中检查 OneToOneField 是否为 None

我有两个这样的模型:

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




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

如果用户有 Type1或 Type2配置文件,我需要做一些事情:

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

但是,对于没有 type1或 type2配置文件的用户,执行这样的代码会产生以下错误:

Type1Profile matching query does not exist.

如何检查用户的配置文件类型?

谢谢

37441 次浏览

How about using try/except blocks?

def get_profile_or_none(user, profile_cls):


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


return profile

Then, use like this!

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!

I suppose you could use this as a generic function to get any reverse OneToOne instance, given an originating class (here: your profile classes) and a related instance (here: request.user).

To check if the (OneToOne) relation exists or not, you can use the hasattr function:

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

Use select_related!

>>> user = User.objects.select_related('type1profile').get(pk=111)
>>> user.type1profile
None

It's possible to see if a nullable one-to-one relationship is null for a particular model simply by testing the corresponding field on the model for Noneness, but only if you test on the model where the one-to-one relationship originates. For example, given these two 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()

… to see if a Restaurant has a Place, we can use the following code:

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

To see if a Place has a Restaurant, it's important to understand that referencing the restaurant property on an instance of Place raises a Restaurant.DoesNotExist exception if there is no corresponding restaurant. This happens because Django performs a lookup internally using QuerySet.get(). For example:

>>> 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 this scenario, Occam's razor prevails, and the best approach for making a determination about whether or not a Place has a Restautrant would be a standard try / except construct as described here.

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

While joctee's suggestion to use hasattr works in practice, it really only works by accident since hasattr suppresses all exceptions (including DoesNotExist) as opposed to just AttributeErrors, like it should. As Pi Delport pointed out, this behavior was actually corrected in Python 3.2 per the following ticket: http://bugs.python.org/issue9666. Furthermore — and at the risk of sounding opinionated — I believe the above try / except construct is more representative of how Django works, while using hasattr can cloud the issue for newbies, which may create FUD and spread bad habits.

EDIT Don Kirkby's reasonable compromise also seems reasonable to me.

I like joctee's answer, because it's so simple.

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

Other commenters have raised concerns that it may not work with certain versions of Python or Django, but the Django documentation shows this technique as one of the options:

You can also use hasattr to avoid the need for exception catching:

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

Of course, the documentation also shows the exception catching technique:

p2 doesn’t have an associated restaurant:

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

I agree with Joshua that catching the exception makes it clearer what's happening, but it just seems messier to me. Perhaps this is a reasonable compromise?

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

This is just querying the Restaurant objects by place. It returns None if that place has no restaurant.

Here's an executable snippet for you to play with the options. If you have Python, Django, and SQLite3 installed, it should just run. I tested it with Python 3.8.10 and Django 4.0.2.

""" Django models in a single, runnable file.


Based on Nsukami's blog post: https://nskm.xyz/posts/dsfp/


To get it running, copy it into a directory named udjango:
$ pip install django
$ python udjango_models.py


Tested with Django 4.0 and Python 3.8.
"""
import logging
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'
DB_FILE = NAME + '.db'




def main():
setup()


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


def __str__(self):
return "%s the place" % self.name


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


def __str__(self):
return "%s the restaurant" % self.place.name


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


def __str__(self):
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():
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()

I am using a combination of has_attr and is 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

in case you have the Model

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

And you just need to know for any User that UserProfile exists/or not - the most efficient way from the database point of view to use exists query.

Exists query will return just boolean, rather than reverse attribute access like hasattr(request.user, 'type1profile') - which will generate get query and return full object representation

To do it - you need to add a property to the User model

class User(AbstractBaseUser)


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

One of the smart approaches will be to add custom field OneToOneOrNoneField and use it [works for 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)

Implementation

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