In Django - Model Inheritance - Ti consente di sovrascrivere l'attributo di un modello genitore?


99

Sto cercando di fare questo:

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)

Questa è la versione che vorrei usare (anche se sono aperto a qualsiasi suggerimento): http://docs.djangoproject.com/en/dev/topics/db/models/#id7

È supportato in Django? In caso contrario, esiste un modo per ottenere risultati simili?


puoi accettare la risposta qui sotto, da django 1.10 è possibile :)
holms

@holms solo se la classe base è astratta!
Micah Walter

Risposte:


64

Risposta aggiornata: come le persone hanno notato nei commenti, la risposta originale non rispondeva correttamente alla domanda. In effetti, solo il LongNamedRestaurantmodello è stato creato nel database, Placenon lo era.

Una soluzione è creare un modello astratto che rappresenti un "Luogo", ad es. AbstractPlace, ed eredita da esso:

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)

Si prega di leggere anche la risposta di @Mark , fornisce un'ottima spiegazione del perché non è possibile modificare gli attributi ereditati da una classe non astratta.

(Nota questo è possibile solo da Django 1.10: prima di Django 1.10, non era possibile modificare un attributo ereditato da una classe astratta.)

Risposta originale

Da Django 1.10 è possibile ! Devi solo fare quello che hai chiesto:

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
Il posto deve essere astratto, no?
DylanYoung

4
Non credo di aver risposto a una domanda diversa poiché sto solo dicendo che il codice pubblicato nella domanda ora funziona da Django 1.10. Nota che secondo il link che ha pubblicato su ciò che voleva usare, ha dimenticato di rendere astratta la classe Place.
qmarlats

2
Non sono sicuro del motivo per cui questa sia la risposta accettata ... OP sta usando l'ereditarietà multi-tabella. Questa risposta è valida solo per le classi base astratte.
MrName

1
le classi astratte erano disponibili molto prima di Django 1.10
rbennell

1
@NoamG Nella mia risposta originale, Placeera astratto, quindi non è stato creato nel database. Ma OP voleva che entrambi Placee LongNamedRestaurantfossero creati nel database. Pertanto ho aggiornato la mia risposta per aggiungere il AbstractPlacemodello, che è il modello "base" (cioè astratto) Placee LongNamedRestaurantda cui ereditare. Ora entrambi Placee LongNamedRestaurantvengono creati nel database, come richiesto da OP.
qmarlats

61

No, non è :

Il nome del campo "nascondere" non è consentito

Nella normale ereditarietà delle classi Python, è consentito a una classe figlia sovrascrivere qualsiasi attributo della classe genitore. In Django, questo non è consentito per attributi che sono Fieldistanze (almeno, non al momento). Se una classe base ha un campo chiamato author, non è possibile creare un altro campo modello chiamato authorin nessuna classe che eredita da quella classe base.


11
Vedi la mia risposta per capire perché è impossibile. Alle persone piace questo perché ha senso, semplicemente non è immediatamente ovvio.
Contrassegna il

4
@ leo-the-mania, penso che User._meta.get_field('email').required = Truepotrebbe funzionare, non sono sicuro pensato.
Jens Timmerman

@ leo-the-mania, @JensTimmerman, @utapyngo L'impostazione del valore della proprietà della tua classe non avrà effetto sui campi ereditati. Devi agire sulla _metaclasse genitore, ad esempio MyParentClass._meta.get_field('email').blank = False(per rendere emailobbligatorio un campo ereditato
nell'Admin

1
Ops, scusa, il codice di @utapyngo sopra è corretto, ma deve essere posizionato al di fuori del corpo della classe, in seguito! L'impostazione del campo della classe genitore come ho suggerito potrebbe avere effetti collaterali indesiderati.
Peterino

Voglio che un campo in ciascuna delle sottoclassi sia di un tipo diverso da un campo con lo stesso nome nella classe genitore astratta per garantire che tutte le sottoclassi abbiano un campo con un certo nome. Il codice di utapyngo non soddisfa questa esigenza.
Daniel

28

Ciò non è possibile a meno che non sia astratto, ed ecco perché: LongNamedRestaurantè anche a Place, non solo come classe ma anche nel database. Il tavolo del posto contiene una voce per ogni puro Placee per ogni LongNamedRestaurant. LongNamedRestaurantcrea solo una tabella aggiuntiva con food_typee un riferimento alla tabella del luogo.

Se lo fai Place.objects.all(), ottieni anche ogni luogo che è a LongNamedRestaurant, e sarà un'istanza di Place(senza food_type). Quindi Place.namee LongNamedRestaurant.namecondividono la stessa colonna del database e devono quindi essere dello stesso tipo.

Penso che questo abbia senso per i modelli normali: ogni ristorante è un posto e dovrebbe avere almeno tutto ciò che quel posto ha. Forse questa coerenza è anche il motivo per cui non era possibile per i modelli astratti prima della 1.10, anche se lì non avrebbe dato problemi al database. Come osserva @lampslave, è stato reso possibile in 1.10. Consiglierei personalmente attenzione: se Sub.x sostituisce Super.x, assicurati che Sub.x sia una sottoclasse di Super.x, altrimenti Sub non può essere usato al posto di Super.

Soluzioni alternative : è possibile creare un modello utente personalizzato ( AUTH_USER_MODEL) che comporta una discreta duplicazione del codice se è necessario modificare solo il campo dell'email. In alternativa puoi lasciare l'email così com'è e assicurarti che sia richiesta in tutte le forme. Ciò non garantisce l'integrità del database se viene utilizzato da altre applicazioni e non funziona viceversa (se si desidera rendere il nome utente non richiesto).


Immagino sia a causa delle modifiche nella 1.10: "È consentito l'override dei campi del modello ereditati da classi di base astratte". docs.djangoproject.com/en/2.0/releases/1.10/#models
lamplave

Ne dubito poiché non era ancora uscito in quel momento, ma è una buona cosa aggiungere, grazie!
Mark

19

Vedi 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: impossibile impostare l'attributo ((((ma sto provando le scelte impostate
Alexey

Questo non funziona su Django 1.11 (funzionava su versioni precedenti) ... la risposta accettata funziona
acaruci

9

Incollato il codice in una nuova app, aggiunta l'app a INSTALLED_APPS ed eseguito syncdb:

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

Sembra che Django non lo supporti.


7

Questo fantastico pezzo di codice ti consente di "sovrascrivere" i campi nelle classi genitore astratte.

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

Quando i campi sono stati rimossi dalla classe genitore astratta, sei libero di ridefinirli secondo le tue necessità.

Questo non è il mio lavoro. Codice originale da qui: https://gist.github.com/specialunderwear/9d917ddacf3547b646ba


6

Forse potresti occuparti di 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 funziona bene. Non ho provato questo esempio, nel mio caso ho semplicemente sovrascritto un parametro di vincolo quindi ... aspetta e guarda!


1
anche gli argomenti per contrib_to_class sembrano strani (anche nel modo sbagliato?) Sembra che tu l'abbia digitato a memoria. Potresti fornire il codice effettivo che hai testato? Se riuscissi a farlo funzionare, mi piacerebbe sapere esattamente come l'hai fatto.
Michael Bylstra

Questo non funziona per me. Sarei interessato anche a un esempio funzionante.
garromark

si prega di consultare blog.jupo.org/2011/11/10/django-model-field-injection dovrebbe essere contrib_to_class (<ModelClass>, <fieldToReplace>)
goh

3
Place._meta.get_field('name').max_length = 255nel corpo della classe dovrebbe fare il trucco, senza sovrascrivere __init__(). Sarebbe anche più conciso.
Peterino

4

So che è una vecchia domanda, ma ho avuto un problema simile e ho trovato una soluzione alternativa:

Ho seguito le seguenti classi:

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

    class Meta:
        abstract = True

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

Ma volevo che il campo immagine ereditato da Year fosse richiesto mantenendo il campo immagine della superclasse annullabile. Alla fine ho usato ModelForms per rinforzare l'immagine in fase di validazione:

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

Sembra che questo sia applicabile solo per alcune situazioni (sicuramente dove è necessario applicare regole più rigide sul campo della sottoclasse).

In alternativa è possibile utilizzare il clean_<fieldname>()metodo invece di clean(), ad esempio, se townè necessario compilare un campo :

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

Non è possibile sovrascrivere i campi del modello, ma è facilmente ottenibile sovrascrivendo / specificando il metodo clean (). Ho avuto il problema con il campo e-mail e volevo renderlo unico a livello di modello e l'ho fatto in questo modo:

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

Il messaggio di errore viene quindi catturato dal campo Modulo con il nome "email"


La domanda riguarda l'estensione max_length di un campo char. Se questo viene applicato dal database, questa "soluzione" non aiuta. Una soluzione alternativa sarebbe specificare la lunghezza max_length più lunga nel modello di base e utilizzare il metodo clean () per applicare la lunghezza più breve lì.
DylanYoung

0

La mia soluzione è semplice come la prossima monkey patching, nota come ho cambiato il campo max_lengthdell'attributo fo namenel LongNamedRestaurantmodello:

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
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.