W Django - Dziedziczenie modelu - czy pozwala na nadpisanie atrybutu modelu nadrzędnego?

99

Chcę to zrobić:

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)

To jest wersja, której chciałbym użyć (chociaż jestem otwarty na wszelkie sugestie): http://docs.djangoproject.com/en/dev/topics/db/models/#id7

Czy jest to obsługiwane w Django? Jeśli nie, czy istnieje sposób na osiągnięcie podobnych rezultatów?

Johnny 5
źródło
czy możesz przyjąć odpowiedź poniżej, od django 1.10 jest to możliwe :)
holms
@holms tylko wtedy, gdy klasa bazowa jest abstrakcyjna!
Micah Walter

Odpowiedzi:

64

Zaktualizowana odpowiedź: jak zauważyli ludzie w komentarzach, oryginalna odpowiedź nie odpowiadała właściwie na pytanie. Rzeczywiście, tylko LongNamedRestaurantmodel został utworzony w bazie danych, Placenie było.

Rozwiązaniem jest stworzenie abstrakcyjnego modelu reprezentującego „Miejsce”, np. AbstractPlacei odziedziczyć po nim:

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)

Przeczytaj również odpowiedź @Mark , który daje świetne wyjaśnienie, dlaczego nie możesz zmienić atrybutów odziedziczonych z klasy nieabstrakcyjnej .

(Zauważ, że jest to możliwe tylko od Django 1.10: przed Django 1.10 modyfikacja atrybutu dziedziczonego z klasy abstrakcyjnej nie była możliwa).

Oryginalna odpowiedź

Od Django 1.10 jest to możliwe ! Musisz tylko zrobić to, o co prosiłeś:

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
źródło
8
Miejsce musi być abstrakcyjne, prawda?
DylanYoung
4
Myślę, że nie odpowiedziałem na inne pytanie, ponieważ mówię tylko, że kod zamieszczony w pytaniu działa teraz od Django 1.10. Zauważ, że zgodnie z linkiem, który zamieścił na temat tego, czego chciał użyć, zapomniał stworzyć streszczenie klasy Place.
qmarlats
2
Nie wiem, dlaczego jest to akceptowana odpowiedź ... OP korzysta z dziedziczenia wielotabelowego. Ta odpowiedź jest ważna tylko dla abstrakcyjnych klas bazowych.
MrName
1
klasy abstrakcyjne były dostępne na długo przed Django 1.10
rbennell
1
@NoamG W mojej oryginalnej odpowiedzi, Placebyło abstrakcyjne, a więc było nie tworzone w bazie danych. Ale OP chciał zarówno, jak Placei LongNamedRestaurantbyć utworzony w bazie danych. Dlatego zaktualizowałem swoją odpowiedź, dodając AbstractPlacemodel, który jest modelem „bazowym” (tj. Abstrakcyjnym) zarówno jak Placei LongNamedRestaurantdziedziczonym. Teraz oba Placei LongNamedRestaurantsą tworzone w bazie danych, o co poprosił OP.
qmarlats
61

Nie, to nie jest :

„Ukrywanie” nazwy pola jest niedozwolone

W normalnym dziedziczeniu klas Pythona klasa potomna może przesłonić dowolny atrybut z klasy nadrzędnej. W Django nie jest to dozwolone dla atrybutów, które są Fieldinstancjami (przynajmniej nie w tej chwili). Jeśli klasa bazowa ma pole o nazwie author, nie można utworzyć innego pola modelu wywoływanego authorw żadnej klasie, która dziedziczy po tej klasie bazowej.

ptone
źródło
11
Zobacz moją odpowiedź, dlaczego to niemożliwe. Ludzie lubią to, ponieważ ma to sens, po prostu nie jest od razu oczywiste.
Mark
4
@ leo-the-manic Myślę, że User._meta.get_field('email').required = Truemoże zadziałać, nie jestem pewien.
Jens Timmerman
@ leo-the-manic, @JensTimmerman, @utapyngo Ustawienie wartości właściwości Twojej klasy nie będzie miało wpływu na dziedziczone pola. Musisz działać na poziomie _metaklasy nadrzędnej, np. MyParentClass._meta.get_field('email').blank = False(Aby dziedziczone emailpole było obowiązkowe w Admin)
Peterino
1
Ups, przepraszam, powyższy kod @ utapyngo jest poprawny, ale później musi zostać umieszczony poza treścią klasy! Ustawienie pola klasy nadrzędnej zgodnie z sugestią może mieć niepożądane skutki uboczne.
Peterino,
Chcę, aby pole w każdej z podklas było innego typu niż pole o tej samej nazwie w abstrakcyjnej klasie nadrzędnej, aby zagwarantować, że wszystkie podklasy mają pole o określonej nazwie. Kod utapyngo nie spełnia tej potrzeby.
Daniel
28

Nie jest to możliwe, chyba że jest abstrakcyjne, a oto dlaczego: LongNamedRestaurantjest również a Place, nie tylko jako klasa, ale także w bazie danych. Tabela miejsc zawiera wpisy dla wszystkich czystych Placei dla wszystkich LongNamedRestaurant. LongNamedRestaurantpo prostu tworzy dodatkową tabelę z food_typei odniesieniem do tabeli miejsc.

Jeśli to zrobisz Place.objects.all(), otrzymasz również każde miejsce, które jest LongNamedRestauranti będzie to wystąpienie Place(bez food_type). Więc Place.namei LongNamedRestaurant.namedzielić tę samą kolumnę bazy danych, a zatem musi być tego samego typu.

Myślę, że ma to sens w przypadku normalnych modeli: każda restauracja jest miejscem i powinna mieć przynajmniej wszystko, co ma to miejsce. Może ta spójność jest także powodem, dla którego nie było możliwe dla modeli abstrakcyjnych przed 1.10, chociaż nie spowodowałoby tam problemów z bazą danych. Jak zauważa @lampslave, stało się to możliwe w 1.10. Osobiście zalecałbym ostrożność: jeśli Sub.x zastępuje Super.x, upewnij się, że Sub.x jest podklasą Super.x, w przeciwnym razie Sub nie może być używany zamiast Super.x.

Obejście problemu : Możesz utworzyć niestandardowy model użytkownika ( AUTH_USER_MODEL), który wiąże się z duplikacją kodu, jeśli potrzebujesz tylko zmienić pole adresu e-mail. Alternatywnie możesz zostawić wiadomość e-mail bez zmian i upewnić się, że jest wymagana we wszystkich formach. Nie gwarantuje to integralności bazy danych, jeśli używają jej inne aplikacje, i nie działa na odwrót (jeśli chcesz, aby nazwa użytkownika nie była wymagana).

znak
źródło
Wydaje mi się, że to z powodu zmian w 1.10: „Dozwolone nadpisywanie pól modelu dziedziczonych z abstrakcyjnych klas bazowych”. docs.djangoproject.com/en/2.0/releases/1.10/#models
lampslave
Wątpię, ponieważ wtedy jeszcze tego nie było, ale warto to dodać, dzięki!
Mark
19

Zobacz 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
niebieskawy
źródło
2
AttributeError: nie można ustawić atrybut ((((ale staram zestaw wyborów
Alexey
To nie działa na Django 1.11 (działało na poprzednich wersjach) ... zaakceptowana odpowiedź działa
acaruci
9

Wkleiłem kod do nowej aplikacji, dodałem aplikację do INSTALLED_APPS i uruchomiłem syncdb:

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

Wygląda na to, że Django tego nie obsługuje.

Brian Luft
źródło
7

Ten superfajny fragment kodu pozwala na „przesłonięcie” pól w abstrakcyjnych klasach nadrzędnych.

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

Po usunięciu pól z abstrakcyjnej klasy nadrzędnej możesz dowolnie je przedefiniować.

To nie jest moja własna praca. Oryginalny kod stąd: https://gist.github.com/specialunderwear/9d917ddacf3547b646ba

Devin
źródło
6

Może mógłbyś poradzić sobie z contrib_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 działa dobrze. Nie próbowałem tego przykładu, w moim przypadku po prostu nadpisuję parametr ograniczenia, więc ... czekaj i zobacz!

JF Simon
źródło
1
również argumenty do contrib_to_class wydają się dziwne (także w niewłaściwy sposób?) Wygląda na to, że wpisałeś to z pamięci. Czy mógłbyś podać rzeczywisty testowany kod? Gdybyś to działało, chciałbym dokładnie wiedzieć, jak to zrobiłeś.
Michael Bylstra
To nie działa na mnie. Byłby również zainteresowany działającym przykładem.
garromark
proszę zobaczyć blog.jupo.org/2011/11/10/django-model-field-injection , powinno to być contrib_to_class (<ModelClass>, <fieldToReplace>)
goh
3
Place._meta.get_field('name').max_length = 255w treści klasy powinno załatwić sprawę, bez nadpisywania __init__(). Byłoby też bardziej zwięzłe.
Peterino,
4

Wiem, że to stare pytanie, ale miałem podobny problem i znalazłem obejście:

Miałem następujące zajęcia:

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

    class Meta:
        abstract = True

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

Ale chciałem, aby dziedziczone przez rok pole obrazu było wymagane przy jednoczesnym zachowaniu wartości zerowej pola obrazu superklasy. W końcu użyłem ModelForms do wymuszenia obrazu na etapie walidacji:

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

Wygląda na to, że ma to zastosowanie tylko w niektórych sytuacjach (na pewno tam, gdzie trzeba wymusić bardziej rygorystyczne reguły w polu podklasy).

Alternatywnie możesz użyć clean_<fieldname>()metody zamiast clean(), np. Gdyby townwymagane było wypełnienie pola :

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
źródło
1

Nie można przesłonić pól modelu, ale można to łatwo osiągnąć, zastępując / określając metodę clean (). Miałem problem z polem e-mail i chciałem uczynić go wyjątkowym na poziomie Modelu i zrobiłem to w następujący sposób:

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

Komunikat o błędzie jest następnie przechwytywany przez pole formularza o nazwie „e-mail”

Phoenix49
źródło
Pytanie dotyczy rozszerzenia max_length pola char. Jeśli jest to wymuszone przez bazę danych, to „rozwiązanie” nie pomoże. Rozwiązaniem byłoby określenie dłuższej długości max_length w modelu podstawowym i użycie metody clean (), aby wymusić w tym miejscu krótszą długość.
DylanYoung
0

Moje rozwiązanie jest tak proste, jak następne monkey patching, zwróć uwagę, jak zmieniłem max_lengthatrybut namepola w LongNamedRestaurantmodelu:

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
źródło