In Django - Modellvererbung - Ermöglicht es Ihnen, das Attribut eines übergeordneten Modells zu überschreiben?

99

Ich möchte dies tun:

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)

Dies ist die Version, die ich verwenden möchte (obwohl ich für jeden Vorschlag offen bin): http://docs.djangoproject.com/de/dev/topics/db/models/#id7

Wird dies in Django unterstützt? Wenn nicht, gibt es eine Möglichkeit, ähnliche Ergebnisse zu erzielen?

Johnny 5
quelle
Kannst du bitte eine Antwort unten akzeptieren, ab Django 1.10 ist es möglich :)
Holms
@holms nur wenn die Basisklasse abstrakt ist!
Micah Walter

Antworten:

64

Aktualisierte Antwort: Wie in den Kommentaren erwähnt, hat die ursprüngliche Antwort die Frage nicht richtig beantwortet. In der Tat wurde nur das LongNamedRestaurantModell in der Datenbank erstellt, Placenicht.

Eine Lösung besteht darin, ein abstraktes Modell zu erstellen, das einen "Ort" darstellt, z. AbstractPlaceund erben davon:

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)

Bitte lesen Sie auch @ Mark Antwort , er gibt eine gute Erklärung, warum Sie Attribute, die von einer nicht abstrakten Klasse geerbt wurden, nicht ändern können.

(Beachten Sie, dass dies nur seit Django 1.10 möglich ist: Vor Django 1.10 war das Ändern eines von einer abstrakten Klasse geerbten Attributs nicht möglich.)

Ursprüngliche Antwort

Seit Django 1.10 ist es möglich ! Sie müssen nur das tun, wonach Sie gefragt haben:

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
quelle
8
Ort muss abstrakt sein, nein?
DylanYoung
4
Ich glaube nicht, dass ich eine andere Frage beantwortet habe, da ich nur sage, dass der in der Frage veröffentlichte Code jetzt seit Django 1.10 funktioniert. Beachten Sie, dass er gemäß dem Link, den er über das, was er verwenden wollte, gepostet hat, vergessen hat, die Place-Klasse abstrakt zu machen.
Qmarlats
2
Ich bin mir nicht sicher, warum dies die akzeptierte Antwort ist ... OP verwendet die Vererbung mehrerer Tabellen. Diese Antwort gilt nur für abstrakte Basisklassen.
MrName
1
abstrakte Klassen waren lange vor Django 1.10
Rbennell
1
@NoamG In meiner ursprünglichen Antwort Placewar abstrakt, daher wurde es nicht in der Datenbank erstellt. Aber OP wollte beides Placeund LongNamedRestaurantin der Datenbank erstellt werden. Daher habe ich meine Antwort aktualisiert, um das AbstractPlaceModell hinzuzufügen , das sowohl das "Basismodell" (dh das abstrakte Modell) ist Placeund von dem LongNamedRestauranterbt. Jetzt werden beide Placeund LongNamedRestaurantin der Datenbank erstellt, wie OP es verlangt.
Qmarlats
61

Nein, das ist es nicht :

Der Feldname "Verstecken" ist nicht zulässig

Bei der normalen Vererbung von Python-Klassen darf eine untergeordnete Klasse jedes Attribut der übergeordneten Klasse überschreiben. In Django ist dies für Attribute, die FieldInstanzen sind, nicht zulässig (zumindest momentan nicht). Wenn für eine Basisklasse ein Feld aufgerufen wird author, können Sie kein anderes Modellfeld erstellen, das authorin einer Klasse aufgerufen wird , die von dieser Basisklasse erbt.

ptone
quelle
11
Siehe meine Antwort, warum es unmöglich ist. Leute mögen das, weil es Sinn macht, es ist einfach nicht sofort offensichtlich.
Mark
4
@ Leo-the-Manic Ich denke, User._meta.get_field('email').required = Truekönnte funktionieren, nicht sicher gedacht.
Jens Timmerman
@ leo-the-manic, @JensTimmerman, @utapyngo Das Festlegen des Eigenschaftswerts Ihrer Klasse hat keine Auswirkungen auf geerbte Felder. Sie müssen auf die _metader übergeordneten Klasse MyParentClass._meta.get_field('email').blank = Falseemail
reagieren
1
Hoppla, sorry, der obige Code von @ utapyngo ist korrekt, muss aber danach außerhalb des Klassenkörpers platziert werden! Das Festlegen des Felds der übergeordneten Klasse, wie ich vorgeschlagen habe, kann unerwünschte Nebenwirkungen haben.
Peterino
Ich möchte, dass ein Feld in jeder der Unterklassen von einem anderen Typ ist als ein Feld mit demselben Namen in der abstrakten übergeordneten Klasse, um sicherzustellen, dass alle Unterklassen alle ein Feld mit einem bestimmten Namen haben. Der Code von utapyngo erfüllt diese Anforderung nicht.
Daniel
28

Das ist nur möglich, wenn abstrakt, und hier ist der Grund: LongNamedRestaurantist auch eine Place, nicht nur als Klasse, sondern auch in der Datenbank. Die Platztabelle enthält einen Eintrag für jeden Reinen Placeund für jeden LongNamedRestaurant. LongNamedRestaurantErstellt einfach eine zusätzliche Tabelle mit dem food_typeund einem Verweis auf die Ortstabelle.

Wenn Sie dies tun Place.objects.all(), erhalten Sie auch jeden Ort, der ein ist LongNamedRestaurant, und es wird eine Instanz von Place(ohne das food_type) sein. Also Place.nameund LongNamedRestaurant.nameteilen Sie die gleiche Datenbankspalte und müssen daher vom gleichen Typ sein.

Ich denke, das macht für normale Models Sinn: Jedes Restaurant ist ein Ort und sollte zumindest alles haben, was dieser Ort hat. Vielleicht ist diese Konsistenz auch der Grund, warum es für abstrakte Modelle vor 1.10 nicht möglich war, obwohl es dort keine Datenbankprobleme geben würde. Wie @lampslave bemerkt, wurde dies in 1.10 ermöglicht. Ich würde persönlich Vorsicht empfehlen: Wenn Sub.x Super.x überschreibt, stellen Sie sicher, dass Sub.x eine Unterklasse von Super.x ist, andernfalls kann Sub nicht anstelle von Super verwendet werden.

Problemumgehungen : Sie können ein benutzerdefiniertes Benutzermodell ( AUTH_USER_MODEL) erstellen, das eine erhebliche Codeduplizierung erfordert, wenn Sie nur das E-Mail-Feld ändern müssen. Alternativ können Sie die E-Mail unverändert lassen und sicherstellen, dass sie in allen Formularen erforderlich ist. Dies garantiert keine Datenbankintegrität, wenn andere Anwendungen es verwenden, und funktioniert nicht umgekehrt (wenn Sie den Benutzernamen nicht benötigen möchten).

Kennzeichen
quelle
Ich denke, das liegt an Änderungen in 1.10: "Zulässige überschreibende Modellfelder, die von abstrakten Basisklassen geerbt wurden." docs.djangoproject.com/de/2.0/releases/1.10/#models
Lampslave
Ich bezweifle es, da es zu diesem Zeitpunkt noch nicht heraus war, aber das ist eine gute Sache, danke!
Mark
19

Siehe 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
blau gefärbt
quelle
2
AttributeError: Attribut kann nicht festgelegt werden ((((aber ich versuche, Einstellungen festzulegen)
Alexey
Dies funktioniert nicht mit Django 1.11 (früher mit früheren Versionen) ... die akzeptierte Antwort funktioniert
acaruci
9

Fügte Ihren Code in eine neue App ein, fügte eine App zu INSTALLED_APPS hinzu und führte syncdb aus:

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

Sieht so aus, als würde Django das nicht unterstützen.

Brian Luft
quelle
7

Mit diesem supercoolen Code können Sie Felder in abstrakten übergeordneten Klassen überschreiben.

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

Wenn die Felder aus der abstrakten übergeordneten Klasse entfernt wurden, können Sie sie nach Bedarf neu definieren.

Dies ist nicht meine eigene Arbeit. Originalcode von hier: https://gist.github.com/specialunderwear/9d917ddacf3547b646ba

Devin
quelle
6

Vielleicht könnten Sie sich mit contrib_to_class befassen:

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 funktioniert gut. Ich habe dieses Beispiel nicht ausprobiert. In meinem Fall habe ich nur einen Einschränkungsparameter überschrieben, also ... abwarten!

JF Simon
quelle
1
Auch die Argumente für contrib_to_class scheinen seltsam (auch falsch herum?). Scheint, als hätten Sie dies aus dem Speicher eingegeben. Könnten Sie bitte den tatsächlichen Code angeben, den Sie getestet haben? Wenn Sie das zum Laufen bringen würden, würde ich gerne genau wissen, wie Sie es gemacht haben.
Michael Bylstra
Das funktioniert bei mir nicht. Würde mich auch für ein funktionierendes Beispiel interessieren.
Garromark
Bitte sehen Sie blog.jupo.org/2011/11/10/django-model-field-injection, es sollte beitrag_zu_Klasse sein (<ModelClass>, <fieldToReplace>)
goh
3
Place._meta.get_field('name').max_length = 255in der Klasse sollte der Körper den Trick machen, ohne zu überschreiben __init__(). Wäre auch prägnanter.
Peterino
4

Ich weiß, dass es eine alte Frage ist, aber ich hatte ein ähnliches Problem und fand eine Problemumgehung:

Ich hatte folgende Klassen:

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

    class Meta:
        abstract = True

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

Aber ich wollte, dass das geerbte Bildfeld von Year erforderlich ist, während das Bildfeld der Oberklasse nullbar bleibt. Am Ende habe ich ModelForms verwendet, um das Image in der Validierungsphase zu erzwingen:

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

Es scheint, dass dies nur für einige Situationen gilt (sicherlich, wenn Sie strengere Regeln für das Unterklassenfeld durchsetzen müssen).

Alternativ können Sie die clean_<fieldname>()Methode anstelle von verwenden clean(), z. B. wenn ein Feld townausgefüllt werden müsste:

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
quelle
1

Sie können Modellfelder nicht überschreiben, dies kann jedoch leicht durch Überschreiben / Angeben der clean () -Methode erreicht werden. Ich hatte das Problem mit dem E-Mail-Feld und wollte es auf Modellebene einzigartig machen.

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

Die Fehlermeldung wird dann vom Formularfeld mit dem Namen "email" erfasst.

Phoenix49
quelle
Die Frage betrifft die Erweiterung der maximalen Länge eines Zeichenfelds. Wenn dies von der Datenbank erzwungen wird, hilft diese "Lösung" nicht. Eine Problemumgehung wäre, die längere max_length im Basismodell anzugeben und die clean () -Methode zu verwenden, um die kürzere Länge dort zu erzwingen.
DylanYoung
0

Meine Lösung ist so einfach wie die nächste monkey patching. Beachten Sie, wie ich das max_lengthAttribut für das nameFeld im LongNamedRestaurantModell geändert habe :

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
quelle