No Django - Herança de modelo - permite que você sobrescreva um atributo de modelo pai?


99

Estou tentando fazer isso:

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)

Esta é a versão que gostaria de usar (embora esteja aberto a qualquer sugestão): http://docs.djangoproject.com/en/dev/topics/db/models/#id7

Isso é compatível com Django? Se não, existe uma maneira de obter resultados semelhantes?


você pode aceitar a resposta abaixo, do django 1.10 é possível :)
holms

@holms apenas se a classe base for abstrata!
Micah Walter,

Respostas:


64

Resposta atualizada: como as pessoas notaram nos comentários, a resposta original não respondia adequadamente à pergunta. Na verdade, apenas o LongNamedRestaurantmodelo foi criado em banco de dados, Placenão foi.

Uma solução é criar um modelo abstrato representando um "Local", por exemplo. AbstractPlace, e herdar dele:

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)

Leia também a resposta @Mark , ele dá uma ótima explicação de porque você não pode alterar atributos herdados de uma classe não abstrata.

(Observe que isso só é possível desde o Django 1.10: antes do Django 1.10, modificar um atributo herdado de uma classe abstrata não era possível.)

Resposta original

Desde Django 1.10 é possível ! Você apenas tem que fazer o que pediu:

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
O lugar precisa ser abstrato, não?
DylanYoung,

4
Não acho que respondi a uma pergunta diferente, pois estou apenas dizendo que o código postado na pergunta agora está funcionando desde o Django 1.10. Observe que, de acordo com o link que ele postou sobre o que queria usar, ele se esqueceu de tornar a classe Local abstrata.
qmarlats

2
Não sei por que essa é a resposta aceita ... OP está usando herança de várias tabelas. Essa resposta só é válida para classes base abstratas.
MrName

1
classes abstratas estavam disponíveis muito antes do Django 1.10
rbennell

1
@NoamG Na minha resposta original, Placeera abstrato, portanto, foi não criado no banco de dados. Mas o OP queria que ambos Placee LongNamedRestaurantfossem criados no banco de dados. Portanto, atualizei minha resposta para adicionar o AbstractPlacemodelo, que é o modelo “base” (isto é, abstrato) Placee do qual LongNamedRestaurantherda. Agora ambos Placee LongNamedRestaurantsão criados no banco de dados, como OP pediu.
qmarlats

61

Não, não é :

O nome do campo “ocultar” não é permitido

Na herança normal de classes Python, é permitido a uma classe filha substituir qualquer atributo da classe pai. No Django, isso não é permitido para atributos que são Fieldinstâncias (pelo menos, não no momento). Se uma classe base tem um campo chamado author, você não pode criar outro campo de modelo chamado authorem qualquer classe que herde dessa classe base.


11
Veja minha resposta para saber por que é impossível. As pessoas gostam disso porque faz sentido, mas não é imediatamente óbvio.
Marcos

4
@leo-the-manic eu acho que User._meta.get_field('email').required = Truepoderia funcionar, não tenho certeza.
Jens Timmerman

@leo-the-manic, @JensTimmerman, @utapyngo Definir o valor da propriedade de sua classe não afetará os campos herdados. Você tem que agir de acordo com o _metada classe pai, por exemplo MyParentClass._meta.get_field('email').blank = False(para tornar um emailcampo herdado obrigatório no Admin)
Peterino

1
Opa, desculpe, o código de @utapyngo acima está correto, mas tem que ser colocado fora do corpo da classe, depois! Definir o campo da classe pai como sugeri pode ter efeitos colaterais indesejados.
Peterino

Quero que um campo em cada uma das subclasses seja de um tipo diferente de um campo com o mesmo nome na classe pai abstrata, a fim de garantir que todas as subclasses tenham um campo com um determinado nome. O código de utapyngo não atende a essa necessidade.
Daniel

28

Isso não é possível a menos que abstrato, e aqui está o motivo: LongNamedRestauranttambém é um Place, não apenas como uma classe, mas também no banco de dados. A tabela de locais contém uma entrada para cada puro Placee para cada LongNamedRestaurant. LongNamedRestaurantapenas cria uma mesa extra com food_typee uma referência à mesa local.

Se você fizer isso Place.objects.all(), também obterá todos os lugares que são a LongNamedRestaurante será uma instância de Place(sem food_type). Assim, Place.namee LongNamedRestaurant.namecompartilhar a mesma coluna de banco de dados, e deve, portanto, ser do mesmo tipo.

Acho que isso faz sentido para modelos normais: todo restaurante é um lugar, e deve ter pelo menos tudo que esse lugar tem. Talvez essa consistência também seja o motivo pelo qual não era possível para modelos abstratos antes de 1.10, embora não desse problemas de banco de dados lá. Como observou @lampslave, isso foi possível em 1.10. Eu pessoalmente recomendaria cuidado: se Sub.x substituir Super.x, certifique-se de que Sub.x seja uma subclasse de Super.x, caso contrário, Sub não pode ser usado no lugar de Super.

Soluções alternativas : Você pode criar um modelo de usuário personalizado ( AUTH_USER_MODEL) que envolve bastante duplicação de código se precisar apenas alterar o campo de e-mail. Como alternativa, você pode deixar o e-mail como está e verificar se é obrigatório em todos os formulários. Isso não garante a integridade do banco de dados se outros aplicativos o usarem e não funciona ao contrário (se você quiser tornar o nome de usuário não obrigatório).


Acho que é por causa das mudanças em 1.10: "Campos de modelo de substituição permitidos herdados de classes base abstratas." docs.djangoproject.com/en/2.0/releases/1.10/#models
lamplave

Duvido, pois ainda não tinha sido lançado na época, mas é uma boa coisa a acrescentar, obrigado!
Marcos

19

Consulte 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: não é possível definir o atributo ((((mas estou tentando definir opções
Alexey

Isso não funciona no Django 1.11 (costumava funcionar nas versões anteriores) ... a resposta aceita funciona
acaruci

9

Colou seu código em um aplicativo novo, adicionou o aplicativo ao INSTALLED_APPS e executou o syncdb:

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

Parece que o Django não suporta isso.


7

Este código super-cool permite que você 'substitua' campos em classes pai abstratas.

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 os campos forem removidos da classe pai abstrata, você estará livre para redefini-los conforme necessário.

Este não é meu próprio trabalho. Código original aqui: https://gist.github.com/specialunderwear/9d917ddacf3547b646ba


6

Talvez você possa lidar com 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 funciona bem. Eu não tentei este exemplo, no meu caso eu apenas substituí um parâmetro de restrição então ... espere e veja!


1
também os argumentos para contrib_to_class parecem estranhos (também da maneira errada?) Parece que você digitou isso de memória. Você poderia fornecer o código real testado? Se você fez isso funcionar, eu adoraria saber exatamente como você fez isso.
Michael Bylstra

Isso não está funcionando para mim. Também estaria interessado em um exemplo prático.
garromark

consulte blog.jupo.org/2011/11/10/django-model-field-injection deve ser contrib_to_class (<ModelClass>, <fieldToReplace>)
goh

3
Place._meta.get_field('name').max_length = 255no corpo da classe deve fazer o truque, sem substituir __init__(). Seria mais conciso também.
Peterino

4

Eu sei que é uma pergunta antiga, mas tive um problema semelhante e encontrei uma solução alternativa:

Tive as seguintes aulas:

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

    class Meta:
        abstract = True

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

Mas eu queria que o campo de imagem herdado de Year fosse necessário, mantendo o campo de imagem da superclasse anulável. No final, usei ModelForms para impor a imagem na fase de validação:

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

Parece que isso só se aplica a algumas situações (certamente onde você precisa impor regras mais rígidas no campo da subclasse).

Como alternativa, você pode usar o clean_<fieldname>()método em vez de clean(), por exemplo, se um campo tiver townque ser preenchido:

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

Você não pode substituir os campos do modelo, mas é facilmente obtido substituindo / especificando o método clean (). Eu tive o problema com o campo de e-mail e queria torná-lo único no nível do modelo e fiz assim:

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

A mensagem de erro é então capturada pelo campo do formulário com o nome "email"


A questão é sobre como estender max_length de um campo char. Se isso for imposto pelo banco de dados, essa "solução" não ajudará. Uma solução alternativa seria especificar o max_length mais longo no modelo base e usar o método clean () para impor o comprimento mais curto lá.
DylanYoung,

0

Minha solução é tão simples quanto a seguir monkey patching, observe como mudei o max_lengthatributo para o namecampo no LongNamedRestaurantmodelo:

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
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.