У 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? Якщо ні, чи є спосіб досягти подібних результатів?


Чи можете ви прийняти відповідь внизу, з django 1.10 можливо :)
Закривається

@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, змінити атрибут, успадкований від абстрактного класу, було неможливо.)

Оригінальна відповідь

З Джанго 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)

8
Місце має бути абстрактним, ні?
DylanYoung

4
Я не думаю, що я відповів на інше питання, оскільки я просто кажу, що код, розміщений у запитанні, зараз працює з Джанго 1.10. Зауважте, що за посиланням, яке він розмістив, про те, що він хотів використати, він забув зробити клас Place абстрактним.
qmarlats

2
Не впевнений, чому це прийнята відповідь ... ОП використовує багатонаступне успадкування. Ця відповідь справедлива лише для абстрактних базових класів.
MrName

1
абстрактні заняття були доступні задовго до Джанго 1.10
rbennell

1
@NoamG У моїй оригінальній відповіді Placeбуло абстрактно, отже, воно не було створене в базі даних. Але ОП хотіли і того, Placeі LongNamedRestaurantстворити в базі даних. Тому я оновив свою відповідь, щоб додати AbstractPlaceмодель, яка є "базовою" (тобто абстрактною) моделлю Placeі LongNamedRestaurantвід якої успадковується. Тепер і те, Placeі інше LongNamedRestaurantстворюється в базі даних, як вимагала ОП.
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поле обов'язковим для Адміністратора)
Peterino

1
Ой, вибачте, код @ utapyngo вказано вище, але його потрібно розмістити поза корпусом класу! Встановлення поля батьківського класу, як я запропонував, може мати небажані побічні ефекти.
Петріно

Я хочу, щоб поле в кожному з підкласів було іншого типу, ніж поле з тим самим іменем у абстрактному батьківському класі, щоб гарантувати, що всі підкласи мають поле з певним іменем. Код utapyngo не відповідає цій потребі.
Даніель

28

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

Якщо ви це зробите Place.objects.all(), ви також отримаєте кожне місце, яке є 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/uk/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'

Схоже, Джанго цього не підтримує.


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

Можливо, ви могли б мати справу з submit_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
також аргументи для submit_to_class здаються дивними (теж неправильно?), здається, ви набрали це з пам'яті. Чи можете ви надати фактичний тестований код? Якби у вас це вийшло на роботу, я хотів би точно знати, як ви це зробили.
Майкл Більстра

Це не працює для мене. Був би зацікавлений і робочий приклад.
garromark

будь ласка, дивіться blog.jupo.org/2011/11/10/django-model-field-injection це повинно бути prispe_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

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')})

Потім повідомлення про помилку зафіксується полем форми з назвою "email"


Питання стосується розширення максимальної довжини поля char. Якщо це застосовується базою даних, то це "рішення" не допоможе. Обхідним способом було б вказати більшу довжину max_length в базовій моделі та використовувати метод clean () для забезпечення там коротшої довжини.
DylanYoung

0

Моє рішення настільки ж просто, як наступне monkey patching, зверніть увагу, як я змінив max_lengthатрибут для 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
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.