Джанго наследование и полиморфизм с прокси-моделями - PullRequest
0 голосов
/ 25 мая 2018

Я работаю над проектом Django, который я не запустил, и столкнулся с проблемой наследования .
У меня есть большая модель (упрощенная в примере), которая называется MyModel, котораядолжен представлять различные типы элементов.

Все объекты экземпляра MyModel должны иметь одинаковые поля, но поведение методов сильно различается в зависимости от типа элемента.

До этогов тот момент, когда это было разработано с использованием одного поля MyModel с именем item_type.
Затем методы, определенные в MyModel, проверяют это поле и выполняют другую логику, используя множественные выражения, если:

def example_method(self):
    if self.item_type == TYPE_A:
        do_this()
    if self.item_type == TYPE_B1:
        do_that()

Ещеиз подтипов есть много общего, поэтому, скажем, подтипы B и C представляют 1-й уровень наследования.Тогда эти типы имеют подтипы, например, B1, B2, C1, C2 (лучше объяснено в примере кода ниже).

Я бы сказал, что это не лучший подход для выполнения полиморфизма.

Теперь я хочу изменить эти модели для использования реального наследования.

Поскольку все подмодели имеют одинаковые поля, я думаю, что наследование нескольких таблиц не является необходимым.Я думал об использовании моделей прокси , потому что только их поведение должно меняться в зависимости от их типов.

Это псевдо-решение, к которому я пришел:

ITEM_TYPE_CHOICES = (
    (TYPE_A, _('Type A')),
    (TYPE_B1, _('Type B1')),
    (TYPE_B2, _('Type B2')),
    (TYPE_C1, _('Type C1')),
    (TYPE_C2, _('Type C2')))


class MyModel(models.Model):
    item_type = models.CharField(max_length=12, choices=ITEM_TYPE_CHOICES)

    def common_thing(self):
        pass

    def do_something(self):
        pass


class ModelA(MyModel):
    class Meta:
        proxy = True

    def __init__(self, *args, **kwargs):
        super().__init__(self, *args, **kwargs)
        self.item_type = TYPE_A

    def do_something(self):
        return 'Hola'


class ModelB(MyModel):
    class Meta:
        proxy = True

    def common_thing(self):
        pass

class ModelB1(ModelB):
    class Meta:
        proxy = True

    def __init__(self, *args, **kwargs):
        super().__init__(self, *args, **kwargs)
        self.item_type = TYPE_B1

    def do_something(self):
        pass


class ModelB2(ModelB):
    class Meta:
        proxy = True

    def __init__(self, *args, **kwargs):
        super().__init__(self, *args, **kwargs)
        self.item_type = TYPE_B2

    def do_something(self):
        pass

Этоможет сработать, если мы уже знаем тип объекта, над которым мы работаем.
Допустим, мы хотим создать экземпляр объекта MyModel типа C1, тогда мы могли бы просто создать экземпляр ModelC1, и item_type будет настроен правильно.

Проблема в том, как получить правильную модель прокси из общих экземпляров MyModel?

Наиболее распространенный случай - когда мы получаем результат набора запросов: MyModel.objects.all(), все эти объекты являются экземплярами MyModelи они ничего не знают о прокси.

Я видел разные решения, такие как django-polymorphic , но, как я понял, это основано на наследовании нескольких таблиц, не так ли?не так ли?

Несколько SO ответов и пользовательских решений, которые я видел:

, но никто из них не убедил меня на 100% ..

Учитывая, что это может быть распространенным сценарием, кто-нибудь придумал лучшее решение?

Ответы [ 3 ]

0 голосов
/ 25 мая 2018

Я придумал нестандартное решение, вдохновленное этим SO ответом и этим сообщением в блоге :

from django.db import models
from django.dispatch.dispatcher import receiver

ITEM_TYPE_CHOICES = (
  (TYPE_A, _('type_a')),
  (TYPE_B1, _('type_b1')),
  (TYPE_B2, _('type_b2')),
  (TYPE_C1, _('type_c1')),
  (TYPE_C2, _('type_c2')),
)

class MyModel(models.Model):
    item_type = models.CharField(max_length=12, choices=ITEM_TYPE_CHOICES)        
    description = models.TextField(blank=True, null=True)

    def common_thing(self):
        pass

    def do_something(self):
        pass

    # ****************
    # Hacking Django *
    # ****************
    PROXY_CLASS_MAP = {}  # We don't know this yet

    @classmethod
    def register_proxy_class(cls, item_type):
        """Class decorator for registering subclasses."""
        def decorate(subclass):
            cls.PROXY_CLASS_MAP[item_type] = subclass
            return subclass
        return decorate

    def get_proxy_class(self):
        return self.PROXY_CLASS_MAP.get(self.item_type, MyModel)


# REGISTER SUBCLASSES

@MyModel.register_proxy_class(TYPE_A)
class ModelA(MyModel):
    class Meta:
        proxy = True

    def __init__(self, *args, **kwargs):
        super().__init__(self, *args, **kwargs)
        self.item_type = TYPE_A

    def do_something(self):
        pass

# No need to register this, it's never instantiated directly 
class ModelB(MyModel):
    class Meta:
        proxy = True

    def common_thing(self):
        pass

@MyModel.register_proxy_class(TYPE_B1)
class ModelB1(ModelB):
    class Meta:
        proxy = True

    def __init__(self, *args, **kwargs):
        super().__init__(self, *args, **kwargs)
        self.item_type = TYPE_B1

    def do_something(self):
        pass

@MyModel.register_proxy_class(TYPE_B2)
class ModelB2(ModelB):
    class Meta:
        proxy = True

    def __init__(self, *args, **kwargs):
        super().__init__(self, *args, **kwargs)
        self.item_type = TYPE_B2

    def do_something(self):
        pass


# USING SIGNAL TO CHANGE `__class__` at runtime

@receiver(models.signals.post_init, sender=MyModel)
def update_proxy_object(sender, **kwargs):
    instance = kwargs['instance']
    if hasattr(instance, "get_proxy_class") and not instance._meta.proxy:
        proxy_class = instance.get_proxy_class()
        if proxy_class is not None:
            instance.__class__ = proxy_class

Я использую декоратор register_proxy_classчтобы зарегистрировать каждый подкласс после объявления MyModel, в противном случае мне нужно было бы явно объявить карту {type: subclass} внутри MyModel.Это было бы плохо:

  1. , потому что при объявлении мы не можем ссылаться ни на один из подклассов прокси из MyModel (мы могли бы решить их с помощью строковых имен)
  2. родитель знал быего подклассов, который нарушает принципы ООП.

Как это работает :

Используя декоратор @register_proxy_class(type), каждый подкласс регистрирует себя, фактически создавая запись вMyModel.PROXY_CLASS_MAP диктуется при загрузке модуля.

Затем update_proxy_object выполняется всякий раз, когда MyModel отправляет сигнал post_init.Он изменяет __class__ из MyModel экземпляров во время выполнения, чтобы выбрать правильный подкласс прокси.

В общем:

# a1: MyModel dispatch a post_init signal -> `update_proxy_object` set the proper instance __class__ = ModelA
# Do NOT call ModelA.__init__
a1 = MyModel(item_type=TYPE_A)  
isinstance(a1, MyModel) # True
isinstance(a1, ModelA)  # True

# a2: calls ModelA.__init__ that call the parent MyModel.__init__ then it sets up the item_type for us
a2 = ModelA() # <- no need to pass item_type
isinstance(a2,MyModel) # True
isinstance(a2, ModelA)  #True

# Using custom managers of MyModel return all objects having item_type == 'TYPE_B1'
b1 = MyModel.objects.b1()[0]  # get the first one
isinstance(b1, ModelB1)  # True
isinstance(b1, ModelB)   # True
isinstance(b1, MyModel)  # True
isinstance(b1, ModelA)   # False

Кажется, пока работает, но я немного поэкспериментируюбольше о возможных проблемах, о которых я не думаю.

Круто!

0 голосов
/ 23 августа 2018

Когда вы используете django-polymorphic в своей базовой модели, вы получаете это поведение приведения бесплатно:

class MyModel(PolymorphicModel):
    pass

Каждая модель, которая выходит из нее (прокси-модель или конкретная модель), будет отлита.вернуться к этой модели, когда вы делаете MyModel.objects.all()

0 голосов
/ 25 мая 2018

У меня мало опыта работы с прокси-серверами модели, поэтому я не могу сказать, будет ли это работать должным образом (не имея в виду ничего, что я имею в виду), и насколько это может быть сложно, но вы можете использовать отображение item_type:ProxyClass и переопределить набор запросов вашей модели (или предоставьте второго менеджера с пользовательским набором запросов и т. д.), который фактически ищет это отображение и создает правильную модель прокси.

Кстати, вы можете захотеть набрать django.models.base.Model.from_db, что (с очень быстрого взгляда на исходный код) кажетсябыть методом, вызываемым QuerySet.populate() для создания экземпляров моделей.Простого переопределения этого метода может быть достаточно для решения проблемы, но и здесь это может также что-то сломать ...

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...