Django抽象模型与常规继承的区别

105
除了语法之外,使用Django抽象模型和在Django模型中使用普通Python继承的区别是什么?它们各有利弊。
更新:我认为我的问题被误解了,我收到了关于抽象模型和继承django.db.models.Model类之间差异的回答。 我实际上想知道继承自Django抽象类(Meta:abstract=True)的模型类与继承自普通Python类(而不是models.Model)的模型类之间的区别。
以下是一个示例:
class User(object):
   first_name = models.CharField(..

   def get_username(self):
       return self.username

class User(models.Model):
   first_name = models.CharField(...

   def get_username(self):
       return self.username

   class Meta:
       abstract = True

class Employee(User):
   title = models.CharField(...

1
这是一个很好的概述,介绍了使用两种继承方法之间的权衡。http://charlesleifer.com/blog/django-patterns-model-inheritance/ - jpotts18
6个回答

177

我实际上想知道继承 Django 抽象类(Meta: abstract = True)的模型类和继承普通 Python 类(比如 'object' 而不是 models.Model)的模型类之间的区别。

Django 只会为继承 models.Model 的子类生成表格,因此前者...

class User(models.Model):
   first_name = models.CharField(max_length=255)

   def get_username(self):
       return self.username

   class Meta:
       abstract = True

class Employee(User):
   title = models.CharField(max_length=255)

...将会生成一个单独的表,类似于...

CREATE TABLE myapp_employee
(
    id         INT          NOT NULL AUTO_INCREMENT,
    first_name VARCHAR(255) NOT NULL,
    title      VARCHAR(255) NOT NULL,
    PRIMARY KEY (id)
);

...而后者则...

class User(object):
   first_name = models.CharField(max_length=255)

   def get_username(self):
       return self.username

class Employee(User):
   title = models.CharField(max_length=255)

...不会生成任何表格。

您可以使用多重继承来实现这样的操作...

class User(object):
   first_name = models.CharField(max_length=255)

   def get_username(self):
       return self.username

class Employee(User, models.Model):
   title = models.CharField(max_length=255)

...这将创建一个表,但它将忽略在User类中定义的字段,因此您最终会得到这样的表...

CREATE TABLE myapp_employee
(
    id         INT          NOT NULL AUTO_INCREMENT,
    title      VARCHAR(255) NOT NULL,
    PRIMARY KEY (id)
);

4
谢谢,这回答了我的问题。还不清楚为什么Employee没有创建first_name。 - rpq
5
你需要查看 Django 的源代码,但我记得它在 models.Model 中使用了一些元类技巧,因此超类中定义的字段不会被识别(除非它们也是 models.Model 的子类)。 - Aya
1
@rpq 我知道你是7年前问的,但在 ModelBase__new__ 调用中,它会对类的属性进行初始检查,检查属性值是否具有 contribute_to_class 属性 - 在 Django 中定义的所有 Fields 都具有该属性,因此它们包含在一个名为 contributable_attrs 的特殊字典中,在迁移期间最终传递以生成 DDL。 - Yu Chen
2
每次我看到Django,我就想哭,为什么????为什么这些废话,为什么这个框架的行为方式必须完全不同于语言?最少惊奇原则呢?这种行为(像Django中几乎所有其他内容一样)不是任何理解Python的人所期望的。 - E.Serra

43

使用抽象模型创建一个表格,对于每个子级都有完整的列集,而使用“普通”Python继承则创建一组链接表格(也称为“多表继承”)。考虑下面这种情况:

假设您有两个模型:

class Vehicle(models.Model):
  num_wheels = models.PositiveIntegerField()


class Car(Vehicle):
  make = models.CharField(…)
  year = models.PositiveIntegerField()
如果Vehicle是一个抽象模型,那么你只会有一张表:
app_car:
| id | num_wheels | make | year

然而,如果您使用纯Python继承,则会有两个表:

app_vehicle:
| id | num_wheels

app_car:
| id | vehicle_id | make | model

其中,vehicle_id 是指向 app_vehicle 表中一行的链接,该行还将具有车辆的轮数。

现在,Django 会将其组合成对象形式,以便您可以将 num_wheels 作为 Car 上的属性访问,但是数据库中的基本表示将不同。


更新

针对您更新的问题,从 Django 抽象类继承和从 Python 的 object 继承之间的区别在于前者被视为数据库对象(因此将表同步到数据库),并且它具有 Model 的行为。从纯Python object 继承不会给类(及其子类)带来这些特性。


1
使用 models.OneToOneField(Vehicle) 也相当于继承一个模型类,对吗?这会导致两个单独的表,是吗? - Rafay
1
@MohammadRafayAleem:是的,但是在使用继承时,Django会将num_wheels作为car的属性,而使用OneToOneField时,您需要自己进行解引用。 - mipadi
1
如何使用常规继承强制重新生成app_car表的所有字段,以便它也具有num_wheels字段,而不是具有vehicle_id指针? - Don Grem
@DorinGrecu:将基类设为抽象模型,然后从中继承。 - mipadi
@mipadi,问题在于我无法将基类设为抽象类,因为它在整个项目中都被使用了。 - Don Grem
@DorinGrecu:在这种情况下,子类将需要链接回父类。不过,Django会自动处理这个问题。 - mipadi

25
主要区别在于如何创建模型的数据库表。 如果在使用继承时没有使用abstract = True,Django将为父模型和子模型分别创建单独的表,这些表保存每个模型中定义的字段。
如果为基类使用abstract = True,Django将只为从基类继承的类创建表 - 不管这些字段是在基类还是在继承类中定义的。
利弊取决于您的应用程序架构。给出以下示例模型:
class Publishable(models.Model):
    title = models.CharField(...)
    date = models.DateField(....)

    class Meta:
        # abstract = True

class BlogEntry(Publishable):
    text = models.TextField()


class Image(Publishable):
    image = models.ImageField(...)
如果Publishable类不是抽象的,Django将为可发布项创建一个表格,其中包含titledate列,并为BlogEntryImage分别创建表格。这种解决方案的优点是,您可以查询基本模型中定义的所有字段的所有可发布项,无论它们是博客条目还是图片。但是,如果您例如查询图像,则Django将不得不进行连接操作......如果将Publishable设置为abstract = True,Django将不会为Publishable创建表格,而只会为包含所有字段(包括继承的字段)的博客条目和图片创建表格。这很方便,因为不需要进行这样的操作来获取连接。另请参阅Django关于模型继承的文档

1
这是最好的答案,涵盖了abstract = True和abstract = False。 - F.Tamy

10

我想补充一些其他答案中没有提到的内容。

与Python类不同,使用模型继承时不允许隐藏字段名称

例如,我曾经遇到过以下用例问题:

我有一个从Django的auth PermissionMixin继承的模型:

class PermissionsMixin(models.Model):
    """
    A mixin class that adds the fields and methods necessary to support
    Django's Group and Permission model using the ModelBackend.
    """
    is_superuser = models.BooleanField(_('superuser status'), default=False,
        help_text=_('Designates that this user has all permissions without '
                    'explicitly assigning them.'))
    groups = models.ManyToManyField(Group, verbose_name=_('groups'),
        blank=True, help_text=_('The groups this user belongs to. A user will '
                                'get all permissions granted to each of '
                                'his/her group.'))
    user_permissions = models.ManyToManyField(Permission,
        verbose_name=_('user permissions'), blank=True,
        help_text='Specific permissions for this user.')

    class Meta:
        abstract = True

    # ...

然后我有我的mixin,其中我希望它覆盖groups字段的related_name。所以它大致是这样的:

class WithManagedGroupMixin(object):
    groups = models.ManyToManyField(Group, verbose_name=_('groups'),
        related_name="%(app_label)s_%(class)s",
        blank=True, help_text=_('The groups this user belongs to. A user will '
                            'get all permissions granted to each of '
                            'his/her group.'))

我使用了以下两个mixin:

class Member(PermissionMixin, WithManagedGroupMixin):
    pass

所以,我本来期望这个能够工作,但它没有。问题更加严重的是,我收到的错误根本没有指向模型,我不知道出了什么问题。

在尝试解决这个问题的时候,我随意决定改变我的mixin并将其转换为一个抽象模型mixin。错误变成了这样:

django.core.exceptions.FieldError: Local field 'groups' in class 'Member' clashes with field of similar name from base class 'PermissionMixin'

正如您所看到的,这个错误确实解释了正在发生的事情。

这是一个巨大的区别,在我看来 :)


我有一个与主要问题无关的问题,但是你最后是如何实现这个(覆盖groups字段的related_name)的? - kalo
@kalo 如果我没记错的话,我只是把名称改成了完全不同的东西。没有办法删除或替换继承字段。 - Adrián
是的,当我发现可以使用contribute_to_class方法完成它时,我差点放弃了。 - kalo

2
主要区别在于继承User类的时候。一个版本会像简单类一样运行,另一个版本则会像Django模型一样运行。
如果继承基本的“object”版本,你的Employee类将只是一个标准类,first_name不会成为数据库表的一部分。你不能创建表单或使用任何其他Django功能。
如果继承models.Model版本,你的Employee类将具有Django Model的所有方法,并且它将继承first_name字段作为可以在表单中使用的数据库字段。
根据文档,Abstract Model“提供了一种在Python级别上因式分解公共信息,同时仍然在数据库级别上每个子模型只创建一个数据库表的方法”。

2

在大多数情况下,我更喜欢使用抽象类,因为它不会创建单独的表格,ORM也不需要在数据库中创建连接。而且在Django中使用抽象类非常简单。

class Vehicle(models.Model):
    title = models.CharField(...)
    Name = models.CharField(....)

    class Meta:
         abstract = True

class Car(Vehicle):
    color = models.CharField()

class Bike(Vehicle):
    feul_average = models.IntegerField(...)

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接