В Django - Наследование модели - позволяет ли вам переопределить атрибут родительской модели?

99

Я собираюсь сделать это:

class Place(models.Model):
   name = models.CharField(max_length=20)
   rating = models.DecimalField()

class LongNamedRestaurant(Place):  # Subclassing `Place`.
   name = models.CharField(max_length=255)  # Notice, I'm overriding `Place.name` to give it a longer length.
   food_type = models.CharField(max_length=25)

Это версия, которую я хотел бы использовать (хотя я открыт для любых предложений): http://docs.djangoproject.com/en/dev/topics/db/models/#id7

Поддерживается ли это в Django? Если нет, есть ли способ добиться аналогичных результатов?

Джонни 5
источник
не могли бы вы принять ответ ниже, из django 1.10 это возможно :)
holms
@holms, только если базовый класс абстрактный!
Мика Уолтер

Ответы:

64

Обновленный ответ: как люди отметили в комментариях, исходный ответ не отвечал должным образом на вопрос. Действительно, LongNamedRestaurantв базе данных создавалась только модель, Placeне было.

Решение состоит в том, чтобы создать абстрактную модель, представляющую «Место», например. AbstractPlace, и наследовать от него:

class AbstractPlace(models.Model):
    name = models.CharField(max_length=20)
    rating = models.DecimalField()

    class Meta:
        abstract = True

class Place(AbstractPlace):
    pass

class LongNamedRestaurant(AbstractPlace):
    name = models.CharField(max_length=255)
    food_type = models.CharField(max_length=25)

Также прочитайте ответ @Mark , он дает отличное объяснение, почему вы не можете изменить атрибуты, унаследованные от неабстрактного класса.

(Обратите внимание, что это возможно только с Django 1.10: до Django 1.10 изменение атрибута, унаследованного от абстрактного класса, было невозможно.)

Оригинальный ответ

Начиная с Django 1.10 это возможно ! Вам просто нужно сделать то, о чем вы просили:

class Place(models.Model):
    name = models.CharField(max_length=20)
    rating = models.DecimalField()

    class Meta:
        abstract = True

class LongNamedRestaurant(Place):  # Subclassing `Place`.
    name = models.CharField(max_length=255)  # Notice, I'm overriding `Place.name` to give it a longer length.
    food_type = models.CharField(max_length=25)
qmarlats
источник
8
Место должно быть абстрактным, не так ли?
DylanYoung 02
4
Не думаю, что я ответил на другой вопрос, поскольку я просто говорю, что код, опубликованный в вопросе, теперь работает с Django 1.10. Обратите внимание, что, судя по ссылке, которую он опубликовал о том, что он хотел использовать, он забыл сделать класс Place абстрактным.
qmarlats
2
Не уверен, почему это принятый ответ ... OP использует многотабличное наследование. Этот ответ действителен только для абстрактных базовых классов.
MrName
1
абстрактные классы были доступны задолго до Django 1.10
rbennell
1
@NoamG В моем первоначальном ответе он Placeбыл абстрактным, поэтому он не был создан в базе данных. Но OP хотел, чтобы и то, Placeи другое LongNamedRestaurantбыло создано в базе данных. Поэтому я обновил свой ответ, добавив AbstractPlaceмодель, которая является «базовой» (т. Е. Абстрактной) моделью Placeи LongNamedRestaurantунаследована от нее. Теперь оба Placeи LongNamedRestaurantсоздаются в базе данных, как просил OP.
qmarlats
61

Нет, это не так :

Имя поля «скрытие» не допускается.

При обычном наследовании классов Python дочернему классу разрешается переопределять любой атрибут из родительского класса. В Django это не разрешено для атрибутов, являющихся Fieldэкземплярами (по крайней мере, на данный момент). Если у базового класса есть поле с именем author, вы не можете создать другое поле модели, вызываемое authorв любом классе, который наследуется от этого базового класса.

птон
источник
11
Смотрите мой ответ, почему это невозможно. Людям это нравится, потому что это имеет смысл, просто это не сразу очевидно.
Марк
4
@ leo-the-manic Я думаю, User._meta.get_field('email').required = Trueможет сработать, не совсем уверена.
Йенс Тиммерман,
@ leo-the-manic, @JensTimmerman, @utapyngo Установка значения свойства вашего класса не повлияет на унаследованные поля. Вы должны действовать в _metaсоответствии с родительским классом, например MyParentClass._meta.get_field('email').blank = False(чтобы сделать унаследованное emailполе обязательным в Admin)
Петерино
1
Ой, извините, приведенный выше код @ utapyngo верен, но впоследствии его нужно разместить за пределами тела класса! Установка поля родительского класса, как я предлагал, может иметь нежелательные побочные эффекты.
Петерино
Я хочу, чтобы поле в каждом из подклассов было другого типа, чем поле с тем же именем в абстрактном родительском классе, чтобы гарантировать, что все подклассы имеют поле с определенным именем. Код utapyngo не удовлетворяет этой потребности.
Дэниел
28

Это невозможно, если не абстрактно, и вот почему: LongNamedRestaurantтакже является a Place, не только как класс, но и в базе данных. Таблица мест содержит запись для каждого чистого Placeи для каждого LongNamedRestaurant. LongNamedRestaurantпросто создает дополнительную таблицу с символом food_typeи ссылкой на таблицу мест.

Если вы это сделаете Place.objects.all(), вы также получите каждое место, которое является a LongNamedRestaurant, и оно будет экземпляром Place(без food_type). Таким образом, они используют один Place.nameи LongNamedRestaurant.nameтот же столбец базы данных и, следовательно, должны быть одного типа.

Я думаю, это имеет смысл для обычных моделей: каждый ресторан - это место, и в нем должно быть по крайней мере все, что есть в нем. Возможно, эта согласованность также является причиной того, что абстрактные модели были невозможны до 1.10, хотя это не создавало проблем с базой данных. Как отмечает @lampslave, это стало возможным в 1.10. Я лично рекомендую соблюдать осторожность: если Sub.x переопределяет Super.x, убедитесь, что Sub.x является подклассом Super.x, иначе Sub не может использоваться вместо Super.

Обходные пути : вы можете создать собственную модель пользователя ( AUTH_USER_MODEL), которая включает в себя довольно много дублирования кода, если вам нужно только изменить поле электронной почты. В качестве альтернативы вы можете оставить электронное письмо как есть и убедиться, что оно требуется во всех формах. Это не гарантирует целостность базы данных, если ее используют другие приложения, и не работает наоборот (если вы хотите, чтобы имя пользователя не требовалось).

отметка
источник
Думаю, это из-за изменений в 1.10: «Разрешено переопределение полей модели, унаследованных от абстрактных базовых классов». docs.djangoproject.com/en/2.0/releases/1.10/#models
lamplave
Я сомневаюсь в этом, так как в то время его еще не было, но стоит добавить, спасибо!
Марк
19

См. Https://stackoverflow.com/a/6379556/15690 :

class BaseMessage(models.Model):
    is_public = models.BooleanField(default=False)
    # some more fields...

    class Meta:
        abstract = True

class Message(BaseMessage):
    # some fields...
Message._meta.get_field('is_public').default = True
голубоглазый
источник
2
AttributeError: не удается установить атрибут ((((но я пытаюсь установить варианты
Алексей
Это не работает в Django 1.11 (раньше оно работало в предыдущих версиях) ... принятый ответ работает
acaruci
9

Вставил свой код в новое приложение, добавил приложение в INSTALLED_APPS и запустил syncdb:

django.core.exceptions.FieldError: Local field 'name' in class 'LongNamedRestaurant' clashes with field of similar name from base class 'Place'

Похоже, Django этого не поддерживает.

Брайан Люфт
источник
7

Этот супер-крутой фрагмент кода позволяет вам «переопределять» поля в абстрактных родительских классах.

def AbstractClassWithoutFieldsNamed(cls, *excl):
    """
    Removes unwanted fields from abstract base classes.

    Usage::
    >>> from oscar.apps.address.abstract_models import AbstractBillingAddress

    >>> from koe.meta import AbstractClassWithoutFieldsNamed as without
    >>> class BillingAddress(without(AbstractBillingAddress, 'phone_number')):
    ...     pass
    """
    if cls._meta.abstract:
        remove_fields = [f for f in cls._meta.local_fields if f.name in excl]
        for f in remove_fields:
            cls._meta.local_fields.remove(f)
        return cls
    else:
        raise Exception("Not an abstract model")

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

Это не моя собственная работа. Исходный код отсюда: https://gist.github.com/specialunderwear/9d917ddacf3547b646ba

Девин
источник
6

Может быть, вы могли бы разобраться с assign_to_class:

class LongNamedRestaurant(Place):

    food_type = models.CharField(max_length=25)

    def __init__(self, *args, **kwargs):
        super(LongNamedRestaurant, self).__init__(*args, **kwargs)
        name = models.CharField(max_length=255)
        name.contribute_to_class(self, 'name')

Syncdb отлично работает. Я не пробовал этот пример, в моем случае я просто переопределил параметр ограничения, так что ... подождите и посмотрите!

Дж. Ф. Саймон
источник
1
Кроме того, аргументы для calculate_to_class кажутся странными (тоже неверно?) Похоже, вы набрали это по памяти. Не могли бы вы предоставить фактический код, который вы тестировали? Если вы все-таки заставили это работать, я хотел бы знать, как именно вы это сделали.
Майкл Билстра
У меня это не работает. Был бы также заинтересован в рабочем примере.
garromark
см. blog.jupo.org/2011/11/10/django-model-field-injection, он должен быть assign_to_class (<ModelClass>, <fieldToReplace>)
goh
3
Place._meta.get_field('name').max_length = 255в теле класса должно работать без переопределения __init__(). Тоже было бы короче.
Петерино
4

Я знаю, что это старый вопрос, но у меня была аналогичная проблема, и я нашел обходной путь:

У меня были следующие занятия:

class CommonInfo(models.Model):
    image = models.ImageField(blank=True, null=True, default="")

    class Meta:
        abstract = True

class Year(CommonInfo):
    year = models.IntegerField() 

Но я хотел, чтобы унаследованное от года поле изображения было обязательным, при этом поле изображения суперкласса оставалось обнуляемым. В конце концов, я использовал ModelForms для принудительного применения изображения на этапе проверки:

class YearForm(ModelForm):
    class Meta:
        model = Year

    def clean(self):
        if not self.cleaned_data['image'] or len(self.cleaned_data['image'])==0:
            raise ValidationError("Please provide an image.")

        return self.cleaned_data

admin.py:

class YearAdmin(admin.ModelAdmin):
    form = YearForm

Похоже, это применимо только для некоторых ситуаций (конечно, когда вам нужно ввести более строгие правила в поле подкласса).

В качестве альтернативы вы можете использовать clean_<fieldname>()метод вместо clean(), например, если townнеобходимо заполнить поле :

def clean_town(self):
    town = self.cleaned_data["town"]
    if not town or len(town) == 0:
        raise forms.ValidationError("Please enter a town")
    return town
pholz
источник
1

Вы не можете переопределить поля модели, но это легко достигается путем переопределения / указания метода clean (). У меня была проблема с полем электронной почты, и я хотел сделать его уникальным на уровне модели и сделал это так:

def clean(self):
    """
    Make sure that email field is unique
    """
    if MyUser.objects.filter(email=self.email):
        raise ValidationError({'email': _('This email is already in use')})

Затем сообщение об ошибке записывается в поле формы с именем «электронная почта».

Феникс49
источник
Речь идет о расширении max_length поля char. Если это обеспечивается базой данных, то это «решение» не помогает. Обходной путь - указать более длинную max_length в базовой модели и использовать метод clean () для обеспечения более короткой длины там.
DylanYoung 02
0

Мое решение так же просто, как и следующее monkey patching, обратите внимание, как я изменил поле max_lengthатрибута fo nameв LongNamedRestaurantмодели:

class Place(models.Model):
   name = models.CharField(max_length=20)

class LongNamedRestaurant(Place):
    food_type = models.CharField(max_length=25)
    Place._meta.get_field('name').max_length = 255
NoamG
источник