Jak uzyskać dostęp do klas potomnych obiektu w django bez znajomości nazwy klasy potomnej?

81

W Django, gdy masz klasę nadrzędną i wiele klas potomnych, które ją dziedziczą, normalnie uzyskujesz dostęp do dziecka poprzez parentclass.childclass1_set lub parentclass.childclass2_set, ale co jeśli nie znam nazwy konkretnej klasy potomnej, której chcę?

Czy istnieje sposób na uzyskanie powiązanych obiektów w kierunku rodzic-> dziecko bez znajomości nazwy klasy podrzędnej?

Gabriel Hurley
źródło
79
@ S.Lott Tego rodzaju odpowiedzi naprawdę się starzeją. To, że nie możesz wymyślić przypadku użycia, nie oznacza, że ​​pytający go nie ma. Jeśli używasz podklas dla dowolnego rodzaju polimorficznego zachowania (wiesz, jedna z głównych domniemanych korzyści OOP?), To pytanie jest bardzo naturalną i oczywistą koniecznością.
Carl Meyer
82
@ S.Lott W takim przypadku możesz poćwiczyć niektóre niegrzeczne wersje, takie jak „Nie jestem pewien, czy rozumiem kontekst. Czy możesz wyjaśnić swój przypadek użycia?”
Carl Meyer

Odpowiedzi:

84

( Aktualizacja : dla Django 1.2 i nowszych, które mogą podążać za zapytaniami select_related w odwrotnych relacjach OneToOneField (a tym samym w dół hierarchii dziedziczenia), dostępna jest lepsza technika, która nie wymaga dodanego real_typepola w modelu nadrzędnym. Jest dostępna jako InheritanceManager w projekt django-model-utils .)

Zwykłym sposobem jest dodanie ForeignKey do ContentType w modelu Parent, który przechowuje typ zawartości odpowiedniej klasy „leaf”. Bez tego może być konieczne wykonanie wielu zapytań w tabelach potomnych, aby znaleźć instancję, w zależności od tego, jak duże jest twoje drzewo dziedziczenia. Oto, jak zrobiłem to w jednym projekcie:

from django.contrib.contenttypes.models import ContentType
from django.db import models

class InheritanceCastModel(models.Model):
    """
    An abstract base class that provides a ``real_type`` FK to ContentType.

    For use in trees of inherited models, to be able to downcast
    parent instances to their child types.

    """
    real_type = models.ForeignKey(ContentType, editable=False)

    def save(self, *args, **kwargs):
        if self._state.adding:
            self.real_type = self._get_real_type()
        super(InheritanceCastModel, self).save(*args, **kwargs)
    
    def _get_real_type(self):
        return ContentType.objects.get_for_model(type(self))
            
    def cast(self):
        return self.real_type.get_object_for_this_type(pk=self.pk)
    
    class Meta:
        abstract = True

Jest to zaimplementowane jako abstrakcyjna klasa bazowa, aby umożliwić wielokrotne użycie; możesz również umieścić te metody i FK bezpośrednio w klasie nadrzędnej w określonej hierarchii dziedziczenia.

To rozwiązanie nie zadziała, jeśli nie możesz zmodyfikować modelu nadrzędnego. W takim przypadku utkniesz, sprawdzając ręcznie wszystkie podklasy.

Carl Meyer
źródło
Dziękuję Ci. To jest piękne i zdecydowanie zaoszczędziło mi czasu.
Spike
Było to bardzo pomocne, ale zastanawiam się, dlaczego chcesz zdefiniować FK z null = True. Skopiowaliśmy kod mniej więcej tak, jak jest, i zostaliśmy ugryzieni przez błąd, który zostałby łatwo wykryty i rozwiązany, gdyby FK był obowiązkowy (zauważ również, że metoda cast () traktuje go jako obowiązkowy).
Shai Berger
1
@ShaiBerger Doskonałe pytanie. Trzy + lata później nie mam pojęcia, dlaczego pierwotnie tak było :-) Edycja w celu usunięcia null = True.
Carl Meyer
2
@sunprophit Musisz uważniej przeczytać ponownie moją odpowiedź. Tak, oczywiście możesz to zrobić self.child_object()używając real_typepola (czyli w powyższym kodzie jako cast()metody), a to zajmie tylko jedno zapytanie dla jednej instancji. Ale jeśli masz zestaw zapytań pełen instancji, staje się to N zapytań. Jedynym sposobem uzyskania danych podklasy dla całego zestawu zapytań obiektów w pojedynczym zapytaniu jest użycie sprzężeń, które robi InheritanceManager.
Carl Meyer
1
Rzeczywiście mój błąd, wstyd mi. Dziękuję, że to zauważyłeś i naprawiłeś.
Antoine Pinsard
23

W Pythonie, mając („nowy styl”) klasę X, możesz pobrać jej (bezpośrednie) podklasy za pomocą X.__subclasses__(), która zwraca listę obiektów klas. (Jeśli chcesz „dalszych potomków”, musisz też zadzwonić__subclasses__ każdą z bezpośrednich podklas itp. Itd. - jeśli potrzebujesz pomocy, jak to zrobić skutecznie w Pythonie, po prostu zapytaj!).

Gdy w jakiś sposób zidentyfikujesz interesującą Cię klasę podrzędną (może wszystkie, jeśli chcesz mieć instancje wszystkich podklas podrzędnych itp.), getattr(parentclass,'%s_set' % childclass.__name__)Powinno pomóc (jeśli nazwa klasy podrzędnej to 'foo', jest to tak samo jak dostęp parentclass.foo_set- nie więcej, nie mniej ). Ponownie, jeśli potrzebujesz wyjaśnień lub przykładów, zapytaj!

Alex Martelli
źródło
1
To świetna informacja (nie wiedziałem o podklasach ), ale uważam, że pytanie jest w rzeczywistości znacznie bardziej specyficzne dla tego, jak modele Django implementują dziedziczenie. Jeśli zapytasz o tabelę „Parent”, odzyskasz instancję klasy Parent. W rzeczywistości może to być instancja SomeChild, ale Django nie wykrywa tego automatycznie (może być kosztowne). Możesz dostać się do instancji SomeChild za pomocą atrybutu w instancji Parent, ale tylko wtedy, gdy wiesz już, że jest to SomeChild, którego chcesz, w przeciwieństwie do innej podklasy klasy Parent.
Carl Meyer
2
Przepraszam, nie jest jasne, kiedy mówię „może w rzeczywistości być przykładem SomeChild”. Obiekt, który masz, jest instancją Parent w Pythonie, ale może mieć powiązany wpis w tabeli SomeChild, co oznacza, że ​​możesz preferować pracę z nim jako instancją SomeChild.
Carl Meyer
To zabawne ... Nie potrzebowałem wtedy tych konkretnych informacji, ale myślałem o innym problemie i okazało się, że jest to dokładnie to, czego potrzebowałem, więc jeszcze raz dziękuję!
Gabriel Hurley
To jest PRAWDZIWA odpowiedź. Koniec końców Django to po prostu Python.
snakesNbronies
5

Rozwiązanie Carla jest dobre, oto jeden sposób, aby zrobić to ręcznie, jeśli istnieje wiele powiązanych klas podrzędnych:

def get_children(self):
    rel_objs = self._meta.get_all_related_objects()
    return [getattr(self, x.get_accessor_name()) for x in rel_objs if x.model != type(self)]

Używa funkcji spoza _meta, która nie ma gwarancji, że będzie stabilna w miarę rozwoju django, ale spełnia swoje zadanie i może być używana w locie, jeśli zajdzie taka potrzeba.

Paul McMillan
źródło
5

Możesz do tego użyć django-polymorphic .

Umożliwia automatyczne rzutowanie klas pochodnych z powrotem do ich rzeczywistego typu. Zapewnia również obsługę administracyjną Django, bardziej wydajną obsługę zapytań SQL oraz model proxy, obsługę inline i formset.

Wydaje się, że podstawowa zasada była wielokrotnie odkrywana (w tym pliszka .specificlub przykłady przedstawione w tym poście). Jednak potrzeba więcej wysiłku, aby upewnić się, że nie powoduje to problemu z N-query lub ładnie integruje się z administratorem, zestawami formularzy / inline lub aplikacjami innych firm.

vdboor
źródło
1
To wygląda dobrze i chcę to wypróbować. Czy podczas migracji wystarczy wypełnić pola polymorphic_ctype odpowiednimi typami zawartości (w przypadku migracji południowej)?
joshua
1
@joshua: tak, to jest dokładnie jedyna rzecz, którą musiałbyś zrobić.
vdboor,
Możesz nieco uściślić swoją odpowiedź, wyjaśniając różnicę w stosunku do podejścia przyjętego w django-model-utils (patrz odpowiedź Carla Meyera).
Ryne Everett
1
Zaakceptowane odpowiedzi są nieaktualne, to jest teraz najlepsza odpowiedź.
Pierre.Sassoulas
2

Oto moje rozwiązanie, ponownie używa, _metawięc nie ma gwarancji, że będzie stabilne.

class Animal(models.model):
    name = models.CharField()
    number_legs = models.IntegerField()
    ...

    def get_child_animal(self):
        child_animal = None
        for r in self._meta.get_all_related_objects():
            if r.field.name == 'animal_ptr':
                child_animal = getattr(self, r.get_accessor_name())
        if not child_animal:
            raise Exception("No subclass, you shouldn't create Animals directly")
        return child_animal

class Dog(Animal):
    ...

for a in Animal.objects.all():
    a.get_child_animal() # returns the dog (or whatever) instance
cerberos
źródło
0

Możesz to osiągnąć, szukając wszystkich pól w obiekcie nadrzędnym, które są instancją django.db.models.fields.related.RelatedManager. Z twojego przykładu wynika, że ​​klasy podrzędne, o których mówisz, nie są podklasami. Dobrze?

Chirayu Patel
źródło
0

Alternatywne podejście z wykorzystaniem serwerów proxy można znaleźć w tym poście na blogu . Podobnie jak inne rozwiązania ma swoje zalety i zobowiązania, które bardzo dobrze ujęte są na końcu wpisu.

Filipe Correia
źródło