在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

@holms仅在基类是抽象的情况下!
米卡·沃尔特

Answers:


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之前,无法修改从抽象类继承的属性。)

原始答案

从Django 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'9

4
我不认为我回答了另一个问题,因为我只是说问题中发布的代码从Django 1.10开始就可以使用了。请注意,根据他发布的有关他想使用的内容的链接,他忘记了将Place类抽象化。
qmarlats

2
不确定为什么这是可接受的答案... OP正在使用多表继承。该答案仅对抽象基类有效。
MrName

1
抽象类早在Django 1.10
rbennell

1
@NoamG在我最初的回答中,它Place是抽象的,因此不是在数据库中创建的。但OP希望双方PlaceLongNamedRestaurant在数据库中创建。因此,我更新了答案以添加AbstractPlace模型,该模型既是“基础”(即抽象)模型,Place又是LongNamedRestaurant继承自该模型的模型。现在,这两个PlaceLongNamedRestaurant都在OP中要求的数据库中创建。
qmarlats

61

不,不是

字段名称“隐藏”是不允许的

在常规的Python类继承中,子类可以覆盖父类的任何属性。在Django中,不允许将这些属性用作Field实例(至少目前是不允许的)。如果基类具有一个名为的字段author,则不能author在从该基类继承的任何类中创建另一个名为的模型字段。


11
请参阅我的答案以了解为什么这是不可能的。人们之所以这样,是因为它确实有意义,只是暂时还不明显。
2013年

4
@ leo-the-manic我认为User._meta.get_field('email').required = True可以,但不确定。
Jens Timmerman

@ leo-the-manic,@ JensTimmerman,@ utapyngo设置类的属性值不会对继承的字段产生影响。您必须对_meta父类进行操作,例如MyParentClass._meta.get_field('email').blank = False(以使继承email字段在Admin中成为必需项)
Peterino

1
糟糕,抱歉,上面的@utapyngo代码是正确的,但之后必须将其放置在类主体之外!按照我的建议设置父类的字段可能会产生不良的副作用。
Peterino 2014年

我希望每个子类中的字段与抽象父类中具有相同名称的字段具有不同的类型,以确保所有子类都具有一个具有特定名称的字段。utapyngo的代码无法满足此需求。
丹尼尔(Daniel)

28

除非是抽象的,否则这是不可能的,这就是为什么:LongNamedRestaurant这也是a Place,不仅作为类,而且在数据库中。位置表包含每个纯项Place和每个项的条目LongNamedRestaurantLongNamedRestaurant只需使用创建一个额外的表格,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/en/2.0/releases/1.10/#models
lampslave

我对此表示怀疑,因为当时尚未发布,但这是一件好事,谢谢!
标记

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:无法设置属性((((但我正在尝试设置选择
Alexey

这在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'

看起来Django不支持该功能。


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

也许您可以处理contribute_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正常工作。我没有尝试过这个例子,就我而言,我只是重写了一个约束参数,所以... wait&see!


1
此外,commit_to_class的参数似乎很奇怪(方法也错误吗?)似乎就像您从内存中键入了该参数。您能否提供您测试过的实际代码?如果您确实做到了这一点,那么我很想知道您是如何做到的。
Michael Bylstra 2013年

这对我不起作用。也会对一个工作示例感兴趣。
garromark

请参阅blog.jupo.org/2011/11/10/django-model-field-injection应该contribute_to_class(<ModelClass>,<fieldToReplace>)

3
Place._meta.get_field('name').max_length = 255在班级的身体中应该做到绝招,而不要压倒一切__init__()。也会更加简洁。
Peterino 2014年

4

我知道这是一个老问题,但是我遇到了类似的问题,并找到了解决方法:

我有以下课程:

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

    class Meta:
        abstract = True

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

但是我希望在保留超类的图像字段为空的同时,需要Year的继承图像字段。最后,我在验证阶段使用了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

您不能覆盖Model字段,但是可以通过覆盖/指定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。如果这是由数据库强制执行的,则此“解决方案”无济于事。一种解决方法是在基本模型中指定较长的max_length并使用clean()方法在此处强制使用较短的长度。
DylanYoung'9

0

我的解决方案与next一样简单monkey patching,请注意如何更改模型中的field max_length属性:nameLongNamedRestaurant

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
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.