Wie greife ich auf die untergeordneten Klassen eines Objekts in Django zu, ohne den Namen der untergeordneten Klasse zu kennen?

80

Wenn Sie in Django eine übergeordnete Klasse und mehrere untergeordnete Klassen haben, die davon erben, greifen Sie normalerweise über parentclass.childclass1_set oder parentclass.childclass2_set auf ein untergeordnetes Element zu. Was ist, wenn ich den Namen der gewünschten untergeordneten Klasse nicht kenne?

Gibt es eine Möglichkeit, die zugehörigen Objekte in die Richtung parent-> child zu bringen, ohne den Namen der untergeordneten Klasse zu kennen?

Gabriel Hurley
quelle
79
@ S.Lott Diese Art von Antworten werden wirklich alt. Nur weil Sie sich keinen Anwendungsfall vorstellen können, heißt das nicht, dass der Fragesteller keinen hat. Wenn Sie Unterklassen für jede Art von polymorphem Verhalten verwenden (Sie wissen, einer der wichtigsten vermeintlichen Vorteile von OOP?), Ist diese Frage eine sehr natürliche und offensichtliche Notwendigkeit.
Carl Meyer
82
@ S.Lott In diesem Fall können Sie einige nicht unhöfliche Versionen üben, z. B. "Ich bin nicht sicher, ob ich den Kontext verstehe. Können Sie Ihren Anwendungsfall erläutern?"
Carl Meyer

Antworten:

84

( Update : Für Django 1.2 und höher, das select_related-Abfragen über umgekehrte OneToOneField-Beziehungen (und damit über Vererbungshierarchien hinweg) folgen kann, steht eine bessere Technik zur Verfügung, für die das hinzugefügte real_typeFeld im übergeordneten Modell nicht erforderlich ist . Sie ist als InheritanceManager im verfügbar django-model-utils- Projekt.)

Die übliche Methode hierfür ist das Hinzufügen eines ForeignKey zu ContentType im übergeordneten Modell, in dem der Inhaltstyp der richtigen "Blatt" -Klasse gespeichert wird. Ohne dies müssen Sie möglicherweise eine ganze Reihe von Abfragen für untergeordnete Tabellen durchführen, um die Instanz zu finden, je nachdem, wie groß Ihr Vererbungsbaum ist. So habe ich es in einem Projekt gemacht:

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

Dies wird als abstrakte Basisklasse implementiert, um sie wiederverwendbar zu machen. Sie können diese Methoden und die FK auch direkt in die übergeordnete Klasse in Ihrer speziellen Vererbungshierarchie einfügen.

Diese Lösung funktioniert nicht, wenn Sie das übergeordnete Modell nicht ändern können. In diesem Fall stecken Sie ziemlich fest und überprüfen alle Unterklassen manuell.

Carl Meyer
quelle
Vielen Dank. Das ist wunderschön und hat mir definitiv Zeit gespart.
Spike
Dies war sehr hilfreich, aber ich frage mich, warum Sie die FK mit null = True definieren möchten. Wir haben den Code mehr oder weniger so wie er ist kopiert und waren von einem Fehler betroffen, der leicht erkannt und behoben worden wäre, wenn die FK obligatorisch gewesen wäre (beachten Sie auch, dass die cast () -Methode ihn als obligatorisch behandelt).
Shai Berger
1
@ShaiBerger Ausgezeichnete Frage. Drei + Jahre später habe ich keine Ahnung, warum es ursprünglich so war :-) Bearbeiten, um die Null = True zu entfernen.
Carl Meyer
2
@sunprophit Sie müssen meine Antwort genauer lesen. Ja, natürlich können Sie self.child_object()das real_typeFeld (das im obigen Code als cast()Methode enthalten ist) verwenden, und es wird nur eine Abfrage für eine Instanz benötigt. Wenn Sie jedoch ein Abfrageset voller Instanzen haben, werden daraus N Abfragen. Die einzige Möglichkeit, die Unterklassendaten für eine ganze Abfragemenge von Objekten in einer einzelnen Abfrage abzurufen, besteht darin, die Joins zu verwenden, die InheritanceManager ausführt.
Carl Meyer
1
Mein Fehler in der Tat, Schande über mich. Vielen Dank, dass Sie es bemerkt und behoben haben.
Antoine Pinsard
22

In Python können Sie bei einer ("neuen") Klasse X ihre (direkten) Unterklassen mit abrufen X.__subclasses__(), die eine Liste von Klassenobjekten zurückgeben. (Wenn Sie "weitere Nachkommen" möchten, müssen Sie auch __subclasses__jede der direkten Unterklassen usw. usw. aufrufen. Wenn Sie Hilfe benötigen, um dies in Python effektiv zu tun, fragen Sie einfach!).

Sobald Sie eine interessierende untergeordnete Klasse identifiziert haben (vielleicht alle, wenn Sie Instanzen aller untergeordneten Unterklassen usw. möchten), getattr(parentclass,'%s_set' % childclass.__name__)sollte dies helfen (wenn der Name der untergeordneten Klasse lautet 'foo', ist dies wie der Zugriff parentclass.foo_set- nicht mehr und nicht weniger ). Wenn Sie weitere Erläuterungen oder Beispiele benötigen, fragen Sie bitte!

Alex Martelli
quelle
1
Dies sind großartige Informationen (ich wusste nichts über Unterklassen ), aber ich glaube, die Frage ist tatsächlich viel spezifischer für die Implementierung der Vererbung durch Django-Modelle. Wenn Sie die Tabelle "Parent" abfragen, erhalten Sie eine Instanz von Parent zurück. Es kann tatsächlich eine Instanz von SomeChild sein, aber Django findet das nicht automatisch für Sie heraus (könnte teuer sein). Sie können über ein Attribut in der übergeordneten Instanz zur SomeChild-Instanz gelangen, jedoch nur, wenn Sie bereits wissen, dass es sich um SomeChild handelt, das Sie möchten, im Gegensatz zu einer anderen Unterklasse von Parent.
Carl Meyer
2
Entschuldigung, es ist nicht klar, wenn ich sage "Kann tatsächlich eine Instanz von SomeChild sein." Das Objekt, über das Sie verfügen, ist eine Instanz von Parent in Python, hat jedoch möglicherweise einen zugehörigen Eintrag in der SomeChild-Tabelle. Dies bedeutet, dass Sie es möglicherweise vorziehen, als SomeChild-Instanz damit zu arbeiten.
Carl Meyer
Es ist lustig ... Ich brauchte diese spezifischen Informationen zu der Zeit nicht, aber ich habe nur über ein anderes Thema nachgedacht und es stellte sich heraus, dass dies genau das war, was ich brauchte. Nochmals vielen Dank!
Gabriel Hurley
Dies ist die echte Antwort. Django ist am Ende des Tages nur Python.
SchlangenNbronies
5

Carls Lösung ist gut. Hier ist eine Möglichkeit, dies manuell zu tun, wenn mehrere verwandte untergeordnete Klassen vorhanden sind:

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

Es verwendet eine Funktion aus _meta, die nicht garantiert stabil ist, wenn sich Django entwickelt, aber es macht den Trick und kann bei Bedarf im laufenden Betrieb verwendet werden.

Paul McMillan
quelle
4

Sie können dafür django-polymorph verwenden .

Damit können abgeleitete Klassen automatisch auf ihren tatsächlichen Typ zurückgesetzt werden. Es bietet auch Django-Administratorunterstützung, effizientere SQL-Abfragebehandlung sowie Unterstützung für Proxy-Modelle, Inlines und Formset.

Das Grundprinzip scheint viele Male neu erfunden zu werden (einschließlich Bachstelzen .specificoder der in diesem Beitrag beschriebenen Beispiele). Es erfordert jedoch mehr Aufwand, um sicherzustellen, dass es nicht zu einem N-Abfrageproblem kommt, oder sich gut in den Administrator, Formsets / Inlines oder Apps von Drittanbietern zu integrieren.

vdboor
quelle
1
Das sieht gut aus und ich möchte es ausprobieren. Reicht es bei der Migration aus, nur die Felder polymorphic_ctype mit den entsprechenden Inhaltstypen auszufüllen (bei einer Südmigration)?
Joshua
1
@joshua: ja, das ist genau das einzige was du tun müsstest.
Vdboor
Sie können Ihre Antwort ein wenig präzisieren, indem Sie den Unterschied zum Ansatz der Django-Modell-Utils erläutern (siehe Carl Meyers Antwort).
Ryne Everett
1
Akzeptierte Antworten sind veraltet, dies ist jetzt die beste Antwort.
Pierre.Sassoulas
2

Hier ist meine Lösung, die wieder verwendet wird und _metadaher nicht garantiert stabil ist.

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
quelle
0

Sie können dies erreichen, indem Sie nach allen Feldern im übergeordneten Element suchen, die eine Instanz von django.db.models.fields.related.RelatedManager sind. Aus Ihrem Beispiel geht hervor, dass die untergeordneten Klassen, über die Sie sprechen, keine Unterklassen sind. Richtig?

Chirayu Patel
quelle
0

Einen alternativen Ansatz mit Proxys finden Sie in diesem Blogbeitrag . Wie die anderen Lösungen hat es seine Vorteile und Verbindlichkeiten, die am Ende des Beitrags sehr gut formuliert sind.

Filipe Correia
quelle