Як отримати доступ до дочірніх класів об’єкта в django, не знаючи назви дочірнього класу?


81

У Django, коли у вас є батьківський клас і кілька дочірніх класів, які успадковуються від нього, ви зазвичай отримуєте доступ до дочірнього за допомогою parentclass.childclass1_set або parentclass.childclass2_set, але що, якщо я не знаю імені конкретного дочірнього класу, який я хочу?

Чи є спосіб отримати пов’язані об’єкти у напрямку батьків-> дочірній матеріал, не знаючи назви дочірнього класу?


79
@ S.Lott Такі відповіді справді старіють. Те, що ви не можете придумати варіант використання, не означає, що у автора немає. Якщо ви використовуєте підкласифікацію для будь-якого виду поліморфної поведінки (ви знаєте, одна з основних передбачуваних переваг ООП?), Це питання є цілком природною і очевидною необхідністю.
Карл Мейер,

82
@ S.Lott У такому випадку сміливо практикуйте деякі негрубі версії, наприклад, "Я не впевнений, що розумію контекст. Чи можете ви пояснити ваш варіант використання?"
Карл Мейер,

Відповіді:


84

( Оновлення : Для Django 1.2 та новіших версій, які можуть слідувати за запитами select_related у зворотних відносинах OneToOneField (і, отже, за ієрархіями спадкування), існує краща техніка, яка не вимагає додавання real_typeполя в батьківській моделі. Він доступний як InheritanceManager у django-model-utils .)

Звичайний спосіб зробити це - додати ForeignKey до ContentType на батьківській моделі, яка зберігає тип вмісту відповідного класу "листя". Без цього вам може знадобитися виконати чимало запитів у дочірніх таблицях, щоб знайти примірник, залежно від того, наскільки великим є ваше дерево успадкування. Ось як я це зробив в одному проекті:

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

Це реалізовано як абстрактний базовий клас, щоб зробити його багаторазовим; ви також можете розмістити ці методи та FK безпосередньо на батьківському класі у вашій ієрархії успадкування.

Це рішення не буде працювати, якщо ви не можете змінити батьківську модель. У цьому випадку ви майже застрягли перевіряти всі підкласи вручну.


Дякую. Це прекрасно і, безумовно, заощадило мені час.
Спайк,

Це було дуже корисно, але мені цікаво, чому ви хочете визначити FK з null = True. Ми скопіювали код більш-менш як є, і нас покусала помилка, яку було б легко виявити та вирішити, якби FK був обов'язковим (зверніть увагу також, що метод cast () розглядає його як обов'язковий).
Shai Berger

1
@ShaiBerger Відмінне запитання. Три роки тому я навіть не уявляю, чому спочатку було так :-) Редагування, щоб видалити null = True.
Carl Meyer

2
@sunprophit Вам потрібно уважніше перечитати мою відповідь. Так, звичайно, ви можете self.child_object()використовувати real_typeполе (яке в наведеному вище коді в якості cast()методу), і це займе лише один запит для одного екземпляра. Але якщо у вас набір запитів, повний екземплярів, це стає N запитів. Єдиний спосіб отримати дані підкласу для цілого набору запитів об’єктів в одному запиті - використовувати об’єднання, які робить InheritanceManager.
Carl Meyer

1
Справді моя помилка, сором мені. Дякуємо, що помітили це і виправили.
Антуан Пінсар

23

У Python, давши ("новий стиль") клас X, ви можете отримати його (прямий) підклас за допомогою X.__subclasses__(), який повертає список об'єктів класу. (Якщо ви хочете "подальших нащадків", вам також доведеться зателефонувати __subclasses__до кожного з прямих підкласів тощо, тощо - якщо вам потрібна допомога щодо того, як ефективно це зробити в Python, просто запитайте!).

Після того, як ви якось визначили дочірній клас інтересів (можливо, усі вони, якщо ви хочете екземпляри всіх дочірніх підкласів тощо), getattr(parentclass,'%s_set' % childclass.__name__)повинні допомогти (якщо дочірній клас називається 'foo', це так само, як доступ parentclass.foo_set- ні більше, ні менше ). Знову ж таки, якщо вам потрібні роз’яснення чи приклади, запитайте!


1
Це чудова інформація (я не знав про підкласи ), але я вважаю, що питання насправді набагато конкретніше, як моделі Django реалізують успадкування. Якщо ви запитаєте таблицю "Parent", ви повернете екземпляр Parent. Це може фактично бути екземпляром SomeChild, але Django не понять , що для вас автоматично (може бути дорогим). Ви можете дістатися до екземпляра SomeChild за допомогою атрибута на батьківському екземплярі, але лише якщо ви вже знаєте, що це SomeChild, який ви хочете, на відміну від іншого підкласу Parent.
Carl Meyer

2
На жаль, незрозуміло, коли я кажу: " насправді може бути екземпляром SomeChild". Об’єкт, який ви маєте, є екземпляром Parent у Python, але він може мати відповідний запис у таблиці SomeChild, що означає, що ви можете віддати перевагу роботі з ним як екземпляру SomeChild.
Карл Мейер,

Це смішно ... На той момент мені не потрібна була ця конкретна інформація, але я просто думав про інше питання, і це виявилося саме тим, що мені потрібно, тож дякую ще раз!
Габріель Харлі

Це РЕАЛЬНА відповідь. Django - це просто Python наприкінці дня.
snakesNbronies

5

Рішення Карла є хорошим, ось один із способів зробити це вручну, якщо є кілька пов’язаних дочірніх класів:

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

Він використовує функцію з _meta, яка не гарантується стабільною в міру розвитку django, але вона робить трюк і може використовуватися на льоту, якщо це необхідно.



5

Для цього можна використовувати django-polymorphic .

Це дозволяє автоматично повертати похідні класи до їх фактичного типу. Він також забезпечує підтримку адміністратора Django, більш ефективну обробку запитів SQL, а також підтримку моделі проксі, вбудованих рядків та набору форм.

Базовий принцип, здається, багато разів винаходили (включаючи Пліску .specificта приклади, викладені в цій публікації). Однак потрібно більше зусиль, щоб переконатися, що це не спричиняє проблем із N-запитом, або приємно інтегруватися з адміністратором, наборами форм / рядками або сторонніми програмами.


1
Це виглядає добре, і я хочу спробувати. При міграції досить просто заповнити поля polymorphic_ctype відповідними типами вмісту (при південній міграції)?
Джошуа

1
@joshua: так, це якраз єдине, що вам довелося б зробити.
vdboor

Ви можете трохи уточнити свою відповідь, пояснивши різницю від підходу, використаного в django-model-utils (див. Відповідь Карла Мейєра).
Ryne Everett

1
Прийняті відповіді застаріли, це найкраща відповідь зараз.
Pierre.Sassoulas

2

Ось моє рішення, воно знову використовує, _metaтому не гарантовано буде стабільним.

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

0

Ви можете досягти цього, шукаючи всі поля в батьківському елементі, які є екземпляром django.db.models.fields.related.RelatedManager. З вашого прикладу здається, що дочірні класи, про які ви говорите, не є підкласами. Правда?


Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.