Controlla se OneToOneField è Nessuno in Django


85

Ho due modelli come questo:

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


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

Devo fare qualcosa se l'utente ha il profilo Type1 o Type2:

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

Tuttavia, per gli utenti che non dispongono di profili di tipo1 o di tipo2, l'esecuzione di codice in questo modo produce il seguente errore:

Type1Profile matching query does not exist.

Come posso controllare il tipo di profilo di un utente?

Grazie

Risposte:


92

Per verificare se la relazione (OneToOne) esiste o meno, puoi utilizzare la hasattrfunzione:

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

4
Grazie per questa soluzione. Sfortunatamente, questo non funziona sempre. Nel caso in cui desideri lavorare select_related()ora o in futuro - o forse anche per essere sicuro di gestire anche altri tipi di magia che potrebbero accadere altrove - devi estendere il test come segue:if hasattr(object, 'onetoonerevrelattr') and object.onetoonerevrelattr != None
class stacker

7
Nota che in Python <3.2, hasattringoierà tutte le eccezioni che si verificano durante la ricerca nel database, e non solo DoesNotExist. Questo probabilmente è rotto, e non quello che vuoi.
Pi Delport

non funziona con Python 2.7. Anche se OneToOne non esiste, restituisce un oggetto django.db.models.fields.related.RelatedManager.
alexpirine

@alartur quale versione di django stai usando?
joctee

Django 1.5. Ma ho risolto il mio problema particolare implementando ciò che volevo fare in un modo completamente diverso.
alexpirine

48

È possibile vedere se una relazione uno a uno annullabile è nulla per un particolare modello semplicemente testando il campo corrispondente sul modello per la Noneness, ma solo se si esegue il test sul modello in cui ha origine la relazione uno-a-uno. Ad esempio, date queste due classi ...

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

... per vedere se Restaurantha una Place, possiamo usare il seguente codice:

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

Per vedere se a Placeha una Restaurant, è importante capire che fare riferimento alla restaurantproprietà su un'istanza di Placesolleva Restaurant.DoesNotExistun'eccezione se non c'è un ristorante corrispondente. Ciò accade perché Django esegue una ricerca internamente utilizzando QuerySet.get(). Per esempio:

>>> 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 questo scenario, prevale il rasoio di Occam e l'approccio migliore per determinare se un Placeha o meno Restautrantsarebbe uno standard try/ exceptcostrutto come descritto qui .

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

Anche se il suggerimento di Joctee di utilizzare hasattrin pratica funziona, funziona davvero solo per sbaglio poiché hasattrsopprime tutte le eccezioni (incluse DoesNotExist) invece delle sole AttributeErrors, come dovrebbe. Come ha sottolineato Pi Delport , questo comportamento è stato effettivamente corretto in Python 3.2 per il seguente ticket: http://bugs.python.org/issue9666 . Inoltre, a rischio di sembrare supponente, credo che il costrutto try/ sopra exceptsia più rappresentativo di come funziona Django, mentre l'uso hasattrpuò offuscare il problema per i neofiti, il che può creare FUD e diffondere cattive abitudini.

EDIT Anche il ragionevole compromesso di Don Kirkby mi sembra ragionevole.


19

Mi piace la risposta di Joctee , perché è così semplice.

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

Altri commentatori hanno espresso la preoccupazione che potrebbe non funzionare con alcune versioni di Python o Django, ma la documentazione di Django mostra questa tecnica come una delle opzioni:

Puoi anche usare hasattr per evitare la necessità di catturare eccezioni:

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

Naturalmente, la documentazione mostra anche la tecnica di cattura delle eccezioni:

p2 non ha un ristorante associato:

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

Sono d'accordo con Joshua sul fatto che cogliere l'eccezione rende più chiaro cosa sta succedendo, ma mi sembra solo più complicato. Forse questo è un ragionevole compromesso?

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

Si tratta solo di interrogare gli Restaurantoggetti in base al luogo. Ritorna Nonese quel posto non ha ristorante.

Ecco uno snippet eseguibile per giocare con le opzioni. Se hai installato Python, Django e SQLite3, dovrebbe essere eseguito. L'ho testato con Python 2.7, Python 3.4, Django 1.9.2 e 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()

10

Che ne dici di usare i blocchi try / tranne?

def get_profile_or_none(user, profile_cls):

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

    return profile

Quindi, usa in questo modo!

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!

Suppongo che potresti usarlo come funzione generica per ottenere qualsiasi istanza OneToOne inversa, data una classe di origine (qui: le tue classi del profilo) e un'istanza correlata (qui: request.user).


3

Usa select_related!

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

2
So che funziona in questo modo, ma questo comportamento di select_related è effettivamente documentato?
Kos

3
L'ho appena provato in Django 1.9.2 e si solleva RelatedObjectDoesNotExist.
Don Kirkby,

1

nel caso tu abbia il modello

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

E hai solo bisogno di sapere per qualsiasi utente che UserProfile esiste / o no: il modo più efficiente dal punto di vista del database per utilizzare la query esistente .

Exists query restituirà solo booleano, piuttosto che un accesso agli attributi inverso come hasattr(request.user, 'type1profile')- che genererà una query get e restituirà la rappresentazione completa dell'oggetto

Per farlo, devi aggiungere una proprietà al modello Utente

class User(AbstractBaseUser)

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

0

Sto usando una combinazione di has_attr ed è 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

0

Uno degli approcci intelligenti sarà aggiungere il campo personalizzato OneToOneOrNoneField e usarlo [funziona per 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)

Implementazione

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

Utilizzo

r = Restaurant(serves_hot_dogs=True, serves_pizza=False)
r.place  # will return None

per django 1.8 devi usare SingleRelatedObjectDescriptorinvece di ReverseOneToOneDescriptorcosì from django.db.models.fields.related import SingleRelatedObjectDescriptor
pymen
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.