模型

模型是关于您数据的唯一、明确的信息来源。它包含您正在存储的数据的基本字段和行为。通常,每个模型都映射到单个数据库表。

基础知识

  • 每个模型都是一个继承自django.db.models.Model的 Python 类。

  • 模型的每个属性都代表一个数据库字段。

  • 有了所有这些,Django 为您提供了一个自动生成的数据库访问 API;参见执行查询

快速示例

此示例模型定义了一个Person,它具有first_namelast_name

from django.db import models


class Person(models.Model):
    first_name = models.CharField(max_length=30)
    last_name = models.CharField(max_length=30)

first_namelast_name是模型的字段。每个字段都指定为类属性,并且每个属性都映射到一个数据库列。

上面的Person模型将创建如下所示的数据库表:

CREATE TABLE myapp_person (
    "id" bigint NOT NULL PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY,
    "first_name" varchar(30) NOT NULL,
    "last_name" varchar(30) NOT NULL
);

一些技术说明

  • 表名myapp_person是根据一些模型元数据自动生成的,但可以覆盖。有关更多详细信息,请参见表名

  • 会自动添加一个id字段,但此行为可以被覆盖。请参见自动主键字段

  • 此示例中的CREATE TABLE SQL 使用 PostgreSQL 语法进行格式化,但值得注意的是,Django 使用针对您在设置文件中指定的数据库后端定制的 SQL。

使用模型

定义模型后,您需要告诉 Django 您将要*使用*这些模型。为此,请编辑您的设置文件并将INSTALLED_APPS设置更改为添加包含您的models.py的模块的名称。

例如,如果应用程序的模型位于模块myapp.models中(这是manage.py startapp脚本为应用程序创建的包结构),INSTALLED_APPS应部分读取为:

INSTALLED_APPS = [
    # ...
    "myapp",
    # ...
]

当您向INSTALLED_APPS添加新的应用程序时,请务必运行manage.py migrate,可以选择先使用manage.py makemigrations为其创建迁移。

字段

模型最重要的部分——也是模型中唯一必需的部分——是它定义的数据库字段列表。字段由类属性指定。注意不要选择与模型 API(如cleansavedelete)冲突的字段名。

示例

from django.db import models


class Musician(models.Model):
    first_name = models.CharField(max_length=50)
    last_name = models.CharField(max_length=50)
    instrument = models.CharField(max_length=100)


class Album(models.Model):
    artist = models.ForeignKey(Musician, on_delete=models.CASCADE)
    name = models.CharField(max_length=100)
    release_date = models.DateField()
    num_stars = models.IntegerField()

字段类型

模型中的每个字段都应该是相应Field类的实例。Django 使用字段类类型来确定以下几点:

  • 列类型,它告诉数据库存储哪种类型的数据(例如INTEGERVARCHARTEXT)。

  • 渲染表单字段时使用的默认 HTML 小部件(例如<input type="text"><select>)。

  • 在 Django 的管理员和自动生成的表单中使用的最小验证要求。

Django 提供了数十种内置字段类型;您可以在模型字段参考中找到完整列表。如果 Django 的内置字段无法满足您的需求,您可以轻松编写自己的字段;参见如何创建自定义模型字段

字段选项

每个字段都采用一组特定于字段的参数(在模型字段参考中进行了说明)。例如,CharField(及其子类)需要一个max_length参数,该参数指定用于存储数据的VARCHAR数据库字段的大小。

所有字段类型还可以使用一组通用参数。所有参数都是可选的。它们在参考中进行了全面解释,但以下是最常用参数的快速摘要:

null

如果为True,Django 将空值存储为数据库中的NULL。默认为False

blank

如果为True,则允许该字段为空。默认为False

请注意,这与null不同。null纯粹与数据库相关,而blank与验证相关。如果字段具有blank=True,则表单验证将允许输入空值。如果字段具有blank=False,则该字段将是必需的。

choices

一个序列的2值元组,一个映射,一个枚举类型或一个可调用对象(它不需要参数并返回任何以前的格式),用作此字段的选择。如果给出此选项,则默认表单小部件将是选择框而不是标准文本字段,并且会将选择限制为给定的选择。

选择列表如下所示:

YEAR_IN_SCHOOL_CHOICES = [
    ("FR", "Freshman"),
    ("SO", "Sophomore"),
    ("JR", "Junior"),
    ("SR", "Senior"),
    ("GR", "Graduate"),
]

注意

每次choices的顺序发生更改时,都会创建一个新的迁移。

每个元组中的第一个元素是将存储在数据库中的值。第二个元素由字段的表单小部件显示。

给定一个模型实例,可以使用get_FOO_display()方法访问具有choices的字段的显示值。例如:

from django.db import models


class Person(models.Model):
    SHIRT_SIZES = {
        "S": "Small",
        "M": "Medium",
        "L": "Large",
    }
    name = models.CharField(max_length=60)
    shirt_size = models.CharField(max_length=1, choices=SHIRT_SIZES)
>>> p = Person(name="Fred Flintstone", shirt_size="L")
>>> p.save()
>>> p.shirt_size
'L'
>>> p.get_shirt_size_display()
'Large'

您也可以使用枚举类以简洁的方式定义choices

from django.db import models


class Runner(models.Model):
    MedalType = models.TextChoices("MedalType", "GOLD SILVER BRONZE")
    name = models.CharField(max_length=60)
    medal = models.CharField(blank=True, choices=MedalType, max_length=10)

更多示例可在模型字段参考中找到。

Django 5.0 中的更改

添加了对映射和可调用对象的支持。

default

字段的默认值。可以是值或可调用对象。如果可调用,则每次创建新对象时都会调用它。

db_default

字段的数据库计算默认值。可以是字面值或数据库函数。

如果同时设置了db_defaultField.default,则在 Python 代码中创建实例时,default将优先。

db_default 仍然会在数据库级别设置,并在 ORM 外部插入行或在迁移中添加新字段时使用。

help_text

要与表单小部件一起显示的额外“帮助”文本。即使您的字段不用于表单,它也对文档很有用。

primary_key

如果为True,则此字段是模型的主键。

如果您没有为模型中的任何字段指定primary_key=True,Django 将自动添加一个IntegerField 来保存主键,因此您不需要在任何字段上设置primary_key=True,除非您想覆盖默认主键行为。更多信息,请参见自动主键字段

主键字段是只读的。如果您更改现有对象的主键值然后保存它,则会与旧对象一起创建一个新对象。例如

from django.db import models


class Fruit(models.Model):
    name = models.CharField(max_length=100, primary_key=True)
>>> fruit = Fruit.objects.create(name="Apple")
>>> fruit.name = "Pear"
>>> fruit.save()
>>> Fruit.objects.values_list("name", flat=True)
<QuerySet ['Apple', 'Pear']>
unique

如果为True,则此字段在整个表中必须唯一。

同样,这些只是对最常见字段选项的简短描述。完整的详细信息可以在常用模型字段选项参考中找到。

自动主键字段

默认情况下,Django 为每个模型提供一个自动递增的主键,其类型在每个应用中由AppConfig.default_auto_field指定,或在DEFAULT_AUTO_FIELD设置中全局指定。例如

id = models.BigAutoField(primary_key=True)

如果您想指定自定义主键,请在一个字段上指定primary_key=True。如果 Django 看到您已显式设置Field.primary_key,它不会添加自动id列。

每个模型都要求恰好有一个字段具有primary_key=True(显式声明或自动添加)。

详细字段名称

ForeignKeyManyToManyFieldOneToOneField之外,每个字段类型都接受一个可选的第一个位置参数——详细名称。如果没有给出详细名称,Django 将使用字段的属性名称自动创建它,并将下划线转换为空格。

在此示例中,详细名称为"person's first name"

first_name = models.CharField("person's first name", max_length=30)

在此示例中,详细名称为"first name"

first_name = models.CharField(max_length=30)

ForeignKeyManyToManyFieldOneToOneField要求第一个参数是模型类,因此请使用verbose_name关键字参数

poll = models.ForeignKey(
    Poll,
    on_delete=models.CASCADE,
    verbose_name="the related poll",
)
sites = models.ManyToManyField(Site, verbose_name="list of sites")
place = models.OneToOneField(
    Place,
    on_delete=models.CASCADE,
    verbose_name="related place",
)

约定是不要大写verbose_name的首字母。Django 会在需要的地方自动大写首字母。

关系

显然,关系数据库的强大功能在于将表彼此关联。Django 提供了定义三种最常见的数据库关系类型的方法:多对一、多对多和一对一。

多对一关系

要定义多对一关系,请使用django.db.models.ForeignKey。您可以像使用任何其他Field类型一样使用它:将其作为模型的类属性包含在内。

ForeignKey 需要一个位置参数:模型相关的类。

例如,如果Car模型有一个Manufacturer——也就是说,一个Manufacturer制造多辆汽车,但每辆Car只有一辆Manufacturer——请使用以下定义

from django.db import models


class Manufacturer(models.Model):
    # ...
    pass


class Car(models.Model):
    manufacturer = models.ForeignKey(Manufacturer, on_delete=models.CASCADE)
    # ...

您还可以创建递归关系(具有与其自身多对一关系的对象)和尚未定义的模型的关系;有关详细信息,请参见模型字段参考

建议(但不是必需)ForeignKey字段(上述示例中的manufacturer)的名称是小写的模型名称。您可以随意命名该字段。例如

class Car(models.Model):
    company_that_makes_it = models.ForeignKey(
        Manufacturer,
        on_delete=models.CASCADE,
    )
    # ...

另请参见

ForeignKey字段接受许多额外参数,这些参数在模型字段参考中进行了说明。这些选项有助于定义关系的工作方式;所有选项都是可选的。

有关访问反向关联对象的详细信息,请参见反向关联对象示例

有关示例代码,请参见多对一关系模型示例

多对多关系

要定义多对多关系,请使用ManyToManyField。您可以像使用任何其他Field类型一样使用它:将其作为模型的类属性包含在内。

ManyToManyField 需要一个位置参数:模型相关的类。

例如,如果一个Pizza有多个Topping对象——也就是说,一个Topping可以在多个比萨饼上,而每个Pizza有多个配料——您可以这样表示

from django.db import models


class Topping(models.Model):
    # ...
    pass


class Pizza(models.Model):
    # ...
    toppings = models.ManyToManyField(Topping)

ForeignKey 一样,您也可以创建 递归关系(对象与自身的多对多关系)和 尚未定义模型的关联关系

建议(但不是必需)ManyToManyField 的名称(上例中的 toppings)用复数形式来描述相关的模型对象集合。

哪个模型具有 ManyToManyField 没有关系,但您应该只在一个模型中添加它,而不是两个模型都添加。

通常,ManyToManyField 实例应该放在将在表单上进行编辑的对象中。在上例中,toppingsPizza 中(而不是 Topping 具有 pizzas ManyToManyField),因为考虑披萨拥有配料比配料属于多个披萨更自然。按照上述设置方式,Pizza 表单将允许用户选择配料。

另请参见

有关完整示例,请参见 多对多关系模型示例

ManyToManyField 字段还接受许多额外的参数,这些参数在 模型字段参考 中进行了说明。这些选项有助于定义关系的工作方式;所有选项都是可选的。

多对多关系中的额外字段

当您只处理多对多关系(例如混合搭配披萨和配料)时,标准的 ManyToManyField 就足够了。但是,有时您可能需要将数据与两个模型之间的关系关联起来。

例如,考虑一个跟踪音乐家所属乐队的应用程序。一个人和他们所属的乐队之间存在多对多关系,因此您可以使用 ManyToManyField 来表示这种关系。但是,您可能需要收集有关成员资格的大量详细信息,例如该人加入乐队的日期。

对于这些情况,Django 允许您指定将用于管理多对多关系的模型。然后,您可以将额外字段添加到中间模型。中间模型使用 ManyToManyFieldthrough 参数与之关联,以指向将充当中介的模型。对于我们的音乐家示例,代码如下所示

from django.db import models


class Person(models.Model):
    name = models.CharField(max_length=128)

    def __str__(self):
        return self.name


class Group(models.Model):
    name = models.CharField(max_length=128)
    members = models.ManyToManyField(Person, through="Membership")

    def __str__(self):
        return self.name


class Membership(models.Model):
    person = models.ForeignKey(Person, on_delete=models.CASCADE)
    group = models.ForeignKey(Group, on_delete=models.CASCADE)
    date_joined = models.DateField()
    invite_reason = models.CharField(max_length=64)

设置中间模型时,您需要显式指定与多对多关系相关的模型的外键。此显式声明定义了两个模型之间的关联方式。

中间模型有一些限制

  • 您的中间模型必须包含一个且只有一个指向源模型的外键(在本例中为 Group),或者您必须使用 ManyToManyField.through_fields 显式指定 Django 应用于该关系的外键。如果您有多个外键并且未指定 through_fields,则会引发验证错误。目标模型(在本例中为 Person)的外键也存在类似的限制。

  • 对于通过中间模型与自身具有多对多关系的模型,允许有两个指向同一模型的外键,但它们将被视为多对多关系的两个(不同)方面。但是,如果有多于两个外键,则也必须像上面一样指定 through_fields,否则会引发验证错误。

现在您已将 ManyToManyField 设置为使用中间模型(在本例中为 Membership),您可以开始创建一些多对多关系了。您可以通过创建中间模型的实例来实现此目的

>>> ringo = Person.objects.create(name="Ringo Starr")
>>> paul = Person.objects.create(name="Paul McCartney")
>>> beatles = Group.objects.create(name="The Beatles")
>>> m1 = Membership(
...     person=ringo,
...     group=beatles,
...     date_joined=date(1962, 8, 16),
...     invite_reason="Needed a new drummer.",
... )
>>> m1.save()
>>> beatles.members.all()
<QuerySet [<Person: Ringo Starr>]>
>>> ringo.group_set.all()
<QuerySet [<Group: The Beatles>]>
>>> m2 = Membership.objects.create(
...     person=paul,
...     group=beatles,
...     date_joined=date(1960, 8, 1),
...     invite_reason="Wanted to form a band.",
... )
>>> beatles.members.all()
<QuerySet [<Person: Ringo Starr>, <Person: Paul McCartney>]>

只要您为任何必需字段指定 through_defaults,您也可以使用 add()create()set() 来创建关系

>>> beatles.members.add(john, through_defaults={"date_joined": date(1960, 8, 1)})
>>> beatles.members.create(
...     name="George Harrison", through_defaults={"date_joined": date(1960, 8, 1)}
... )
>>> beatles.members.set(
...     [john, paul, ringo, george], through_defaults={"date_joined": date(1960, 8, 1)}
... )

您可能更倾向于直接创建中间模型的实例。

如果中间模型定义的自定义 through 表未对 (model1, model2) 对强制唯一性,允许多个值,则 remove() 调用将删除所有中间模型实例

>>> Membership.objects.create(
...     person=ringo,
...     group=beatles,
...     date_joined=date(1968, 9, 4),
...     invite_reason="You've been gone for a month and we miss you.",
... )
>>> beatles.members.all()
<QuerySet [<Person: Ringo Starr>, <Person: Paul McCartney>, <Person: Ringo Starr>]>
>>> # This deletes both of the intermediate model instances for Ringo Starr
>>> beatles.members.remove(ringo)
>>> beatles.members.all()
<QuerySet [<Person: Paul McCartney>]>

clear() 方法可用于删除实例的所有多对多关系

>>> # Beatles have broken up
>>> beatles.members.clear()
>>> # Note that this deletes the intermediate model instances
>>> Membership.objects.all()
<QuerySet []>

建立多对多关系后,您可以发出查询。与普通多对多关系一样,您可以使用多对多相关模型的属性进行查询

# Find all the groups with a member whose name starts with 'Paul'
>>> Group.objects.filter(members__name__startswith="Paul")
<QuerySet [<Group: The Beatles>]>

由于您使用的是中间模型,因此您也可以查询其属性

# Find all the members of the Beatles that joined after 1 Jan 1961
>>> Person.objects.filter(
...     group__name="The Beatles", membership__date_joined__gt=date(1961, 1, 1)
... )
<QuerySet [<Person: Ringo Starr]>

如果您需要访问成员资格信息,您可以通过直接查询 Membership 模型来实现

>>> ringos_membership = Membership.objects.get(group=beatles, person=ringo)
>>> ringos_membership.date_joined
datetime.date(1962, 8, 16)
>>> ringos_membership.invite_reason
'Needed a new drummer.'

另一种访问相同信息的方法是从 Person 对象查询 多对多反向关系

>>> ringos_membership = ringo.membership_set.get(group=beatles)
>>> ringos_membership.date_joined
datetime.date(1962, 8, 16)
>>> ringos_membership.invite_reason
'Needed a new drummer.'

一对一关系

要定义一对一关系,请使用 OneToOneField。您可以像使用任何其他 Field 类型一样使用它:将其作为模型的类属性包含在内。

当某个对象以某种方式“扩展”另一个对象时,这在对象的 primary key 上最有用。

OneToOneField 需要一个位置参数:模型相关的类。

例如,如果您正在构建“地点”数据库,您将构建相当标准的内容,例如地址、电话号码等。然后,如果您想在“地点”数据库之上构建餐厅数据库,而不是重复自身并在 Restaurant 模型中复制这些字段,您可以使 RestaurantPlace 具有 OneToOneField 关系(因为餐厅“是”一个地方;事实上,为了处理这个问题,您通常会使用 继承,这涉及到隐式的一对一关系)。

ForeignKey 一样,可以定义 递归关系,并且可以对 尚未定义的模型 进行引用。

另请参见

有关完整示例,请参见 一对一关系模型示例

OneToOneField 字段还接受可选的 parent_link 参数。

过去,OneToOneField 类会自动成为模型的主键。现在不再如此(尽管您可以手动传入 primary_key 参数)。因此,现在可以在单个模型中拥有多个 OneToOneField 类型的字段。

跨文件的模型

将模型与另一个应用中的模型关联起来完全没有问题。为此,请在定义模型的文件顶部导入相关的模型。然后,在需要的地方引用另一个模型类。例如

from django.db import models
from geography.models import ZipCode


class Restaurant(models.Model):
    # ...
    zip_code = models.ForeignKey(
        ZipCode,
        on_delete=models.SET_NULL,
        blank=True,
        null=True,
    )

字段名称限制

Django 对模型字段名称有一些限制。

  1. 字段名称不能是 Python 保留字,因为这会导致 Python 语法错误。例如

    class Example(models.Model):
        pass = models.IntegerField() # 'pass' is a reserved word!
    
  2. 字段名称不能包含多个连续的下划线,因为 Django 的查询查找语法就是这样工作的。例如

    class Example(models.Model):
        foo__bar = models.IntegerField()  # 'foo__bar' has two underscores!
    
  3. 字段名称不能以下划线结尾,原因类似。

但是,这些限制是可以解决的,因为您的字段名称不必与您的数据库列名称匹配。请参阅 db_column 选项。

SQL 保留字,例如 joinwhereselect是可以用作模型字段名称的,因为 Django 会转义所有底层 SQL 查询中的所有数据库表名和列名。它使用您特定数据库引擎的引用语法。

自定义字段类型

如果现有的模型字段无法满足您的需求,或者您希望利用一些不太常见的数据库列类型,您可以创建自己的字段类。在 如何创建自定义模型字段 中提供了创建您自己的字段的完整内容。

Meta 选项

使用内部 class Meta 为您的模型提供元数据,如下所示

from django.db import models


class Ox(models.Model):
    horn_length = models.IntegerField()

    class Meta:
        ordering = ["horn_length"]
        verbose_name_plural = "oxen"

模型元数据是“任何不是字段的内容”,例如排序选项 (ordering)、数据库表名 (db_table) 或人类可读的单数和复数名称 (verbose_nameverbose_name_plural)。没有一个是必需的,向模型添加 class Meta 是完全可选的。

所有可能的 Meta 选项的完整列表可以在 模型选项参考 中找到。

模型属性

objects

模型最重要的属性是 Manager。它是 Django 模型提供数据库查询操作的接口,用于 检索数据库中的实例。如果未定义自定义 Manager,则默认名称为 objects。管理器只能通过模型类访问,不能通过模型实例访问。

模型方法

在模型上定义自定义方法,为您的对象添加自定义“行级”功能。虽然 Manager 方法旨在执行“表范围”操作,但模型方法应该作用于特定的模型实例。

这是一种将业务逻辑保存在一个地方(模型)中的宝贵技术。

例如,此模型有一些自定义方法

from django.db import models


class Person(models.Model):
    first_name = models.CharField(max_length=50)
    last_name = models.CharField(max_length=50)
    birth_date = models.DateField()

    def baby_boomer_status(self):
        "Returns the person's baby-boomer status."
        import datetime

        if self.birth_date < datetime.date(1945, 8, 1):
            return "Pre-boomer"
        elif self.birth_date < datetime.date(1965, 1, 1):
            return "Baby boomer"
        else:
            return "Post-boomer"

    @property
    def full_name(self):
        "Returns the person's full name."
        return f"{self.first_name} {self.last_name}"

此示例中的最后一个方法是一个 属性

模型实例参考 包含 自动赋予每个模型的方法 的完整列表。您可以覆盖大多数这些方法——请参见下面的 覆盖预定义模型方法——但是有一些方法您几乎总是希望定义。

__str__()

返回任何对象的字符串表示形式的 Python“魔术方法”。每当需要将模型实例强制转换为普通字符串并显示时,Python 和 Django 都会使用它。最值得注意的是,当您在交互式控制台或管理员中显示对象时,就会发生这种情况。

您总是需要定义此方法;默认值并没有什么帮助。

get_absolute_url()

这告诉 Django 如何计算对象的 URL。Django 在其管理员界面中使用它,以及任何需要为对象确定 URL 的时候。

任何具有唯一标识其 URL 的对象都应定义此方法。

覆盖预定义模型方法

还有一组 模型方法 封装了许多您可能想要自定义的数据库行为。特别是,您通常希望更改 save()delete() 的工作方式。

您可以随意覆盖这些方法(以及任何其他模型方法)以更改行为。

覆盖内置方法的一个经典用例是,如果您希望在保存对象时发生某些事情。例如(有关它接受的参数的文档,请参阅 save()

from django.db import models


class Blog(models.Model):
    name = models.CharField(max_length=100)
    tagline = models.TextField()

    def save(self, **kwargs):
        do_something()
        super().save(**kwargs)  # Call the "real" save() method.
        do_something_else()

您还可以阻止保存

from django.db import models


class Blog(models.Model):
    name = models.CharField(max_length=100)
    tagline = models.TextField()

    def save(self, **kwargs):
        if self.name == "Yoko Ono's blog":
            return  # Yoko shall never have her own blog!
        else:
            super().save(**kwargs)  # Call the "real" save() method.

重要的是要记住调用超类方法——那就是 super().save(**kwargs) 部分——以确保对象仍然保存到数据库中。如果您忘记调用超类方法,则默认行为将不会发生,并且数据库将不会被修改。

同样重要的是,您需要传递可以传递给模型方法的参数——这就是 **kwargs 的作用。Django 将不时扩展内置模型方法的功能,添加新的关键字参数。如果您在方法定义中使用 **kwargs,则可以保证您的代码在添加这些参数时会自动支持它们。

如果您希望在 save() 方法中更新字段值,您可能还想将此字段添加到 update_fields 关键字参数中。这将确保在指定 update_fields 时保存该字段。例如

from django.db import models
from django.utils.text import slugify


class Blog(models.Model):
    name = models.CharField(max_length=100)
    slug = models.TextField()

    def save(self, **kwargs):
        self.slug = slugify(self.name)
        if (
            update_fields := kwargs.get("update_fields")
        ) is not None and "name" in update_fields:
            kwargs["update_fields"] = {"slug"}.union(update_fields)
        super().save(**kwargs)

有关详细信息,请参阅 指定要保存的字段

覆盖的模型方法不会在批量操作中调用

请注意,当 使用 QuerySet 批量删除对象 或作为 级联删除 的结果时,对象的 delete() 方法不一定被调用。为了确保自定义删除逻辑得到执行,您可以使用 pre_delete 和/或 post_delete 信号。

不幸的是,当 创建更新 对象时,没有解决方法,因为没有调用 save()pre_savepost_save

执行自定义 SQL

另一种常见模式是在模型方法和模块级方法中编写自定义 SQL 语句。有关使用原始 SQL 的更多详细信息,请参阅有关 使用原始 SQL 的文档。

模型继承

Django 中的模型继承的工作方式与 Python 中的普通类继承的工作方式几乎相同,但仍应遵循页面开头介绍的基本知识。这意味着基类应该继承自 django.db.models.Model

您唯一需要做的决定是,您是否希望父模型成为其自身的模型(具有其自己的数据库表),或者父模型只是持有公共信息的容器,只能通过子模型查看。

Django 中有三种可能的继承方式。

  1. 通常,你只需要使用父类来保存一些不想为每个子模型重复键入的信息。这个类永远不会被单独使用,因此你需要使用抽象基类

  2. 如果你正在对现有模型(可能完全来自另一个应用程序)进行子类化,并且希望每个模型都有自己的数据库表,那么多表继承是最佳选择。

  3. 最后,如果你只想修改模型的 Python 级行为,而无需以任何方式更改模型字段,则可以使用代理模型

抽象基类

当你想将一些公共信息放入其他多个模型时,抽象基类非常有用。你可以编写你的基类并在Meta 类中添加abstract=True。这个模型将不会用于创建任何数据库表。相反,当它用作其他模型的基类时,它的字段将添加到子类的字段中。

一个例子

from django.db import models


class CommonInfo(models.Model):
    name = models.CharField(max_length=100)
    age = models.PositiveIntegerField()

    class Meta:
        abstract = True


class Student(CommonInfo):
    home_group = models.CharField(max_length=5)

Student 模型将有三个字段:nameagehome_groupCommonInfo 模型不能作为普通的 Django 模型使用,因为它是一个抽象基类。它不会生成数据库表或拥有管理器,也不能直接实例化或保存。

从抽象基类继承的字段可以用另一个字段或值覆盖,或者用None删除。

对于许多用途来说,这种类型的模型继承正是你想要的。它提供了一种在 Python 层面提取公共信息的方法,同时在数据库层面上每个子模型仍然只创建一个数据库表。

Meta 继承

创建抽象基类时,Django 会将你在基类中声明的任何Meta内部类作为属性提供。如果子类没有声明自己的Meta类,它将继承父类的Meta。如果子类想要扩展父类的Meta类,它可以对其进行子类化。例如

from django.db import models


class CommonInfo(models.Model):
    # ...
    class Meta:
        abstract = True
        ordering = ["name"]


class Student(CommonInfo):
    # ...
    class Meta(CommonInfo.Meta):
        db_table = "student_info"

Django 对抽象基类的Meta类进行了一项调整:在安装Meta属性之前,它会设置abstract=False。这意味着抽象基类的子类不会自动成为抽象类本身。要创建一个从另一个抽象基类继承的抽象基类,你需要在子类上显式设置abstract=True

有些属性不适合包含在抽象基类的Meta类中。例如,包含db_table 将意味着所有子类(那些没有指定自己的Meta的类)将使用相同的数据库表,这几乎肯定不是你想要的。

由于 Python 继承的工作方式,如果子类从多个抽象基类继承,则默认情况下,只有第一个列出的类的Meta选项将被继承。要从多个抽象基类继承Meta选项,必须显式声明Meta继承。例如

from django.db import models


class CommonInfo(models.Model):
    name = models.CharField(max_length=100)
    age = models.PositiveIntegerField()

    class Meta:
        abstract = True
        ordering = ["name"]


class Unmanaged(models.Model):
    class Meta:
        abstract = True
        managed = False


class Student(CommonInfo, Unmanaged):
    home_group = models.CharField(max_length=5)

    class Meta(CommonInfo.Meta, Unmanaged.Meta):
        pass

多表继承

Django 支持的第二种模型继承类型是层次结构中的每个模型都是一个独立的模型。每个模型都对应于它自己的数据库表,并且可以单独查询和创建。继承关系在子模型及其每个父模型之间引入了链接(通过自动创建的OneToOneField)。例如

from django.db import models


class Place(models.Model):
    name = models.CharField(max_length=50)
    address = models.CharField(max_length=80)


class Restaurant(Place):
    serves_hot_dogs = models.BooleanField(default=False)
    serves_pizza = models.BooleanField(default=False)

Place 的所有字段也将在Restaurant 中可用,尽管数据位于不同的数据库表中。因此,这两种情况都是可能的

>>> Place.objects.filter(name="Bob's Cafe")
>>> Restaurant.objects.filter(name="Bob's Cafe")

如果你有一个既是Place 又是Restaurant 的对象,你可以使用模型名称的小写版本从Place 对象获取到Restaurant 对象

>>> p = Place.objects.get(id=12)
# If p is a Restaurant object, this will give the child class:
>>> p.restaurant
<Restaurant: ...>

但是,如果上例中的p *不是*Restaurant(它是直接作为Place 对象创建的,或者它是其他某个类的父类),则引用p.restaurant 将引发Restaurant.DoesNotExist 异常。

自动生成的连接RestaurantPlaceOneToOneField看起来像这样。

place_ptr = models.OneToOneField(
    Place,
    on_delete=models.CASCADE,
    parent_link=True,
    primary_key=True,
)

你可以通过在Restaurant中声明你自己的OneToOneField并设置parent_link=True来覆盖该字段。

Meta和多表继承

在多表继承的情况下,子类继承父类的Meta类是没有意义的。所有Meta选项都已应用于父类,再次应用它们通常只会导致矛盾的行为(这与抽象基类的情况相反,在抽象基类的情况下,基类本身并不存在)。

因此,子模型无法访问其父类的Meta类。但是,在少数情况下,子类会继承父类的行为:如果子类没有指定ordering属性或get_latest_by属性,它将继承自其父类的这些属性。

如果父类具有排序,而你又不希望子类具有任何自然排序,则可以显式地禁用它。

class ChildModel(ParentModel):
    # ...
    class Meta:
        # Remove parent's ordering effect
        ordering = []

继承和反向关系

因为多表继承使用隐式的OneToOneField来链接子类和父类,所以可以从父类向下移动到子类,如上例所示。但是,这会占用作为ForeignKeyManyToManyField关系的默认related_name值的名称。如果将这些类型的关系放在父模型的子类上,则**必须**在每个这样的字段上指定related_name属性。如果忘记,Django 将引发验证错误。

例如,再次使用上面的Place类,让我们创建一个具有ManyToManyField的另一个子类。

class Supplier(Place):
    customers = models.ManyToManyField(Place)

这将导致错误。

Reverse query name for 'Supplier.customers' clashes with reverse query
name for 'Supplier.place_ptr'.

HINT: Add or change a related_name argument to the definition for
'Supplier.customers' or 'Supplier.place_ptr'.

related_name添加到customers字段如下所示可以解决此错误:models.ManyToManyField(Place, related_name='provider')

代理模型

当使用多表继承时,将为模型的每个子类创建一个新的数据库表。这通常是所需的行为,因为子类需要一个地方来存储基类中不存在的任何附加数据字段。但是,有时你只想更改模型的 Python 行为——也许是为了更改默认管理器或添加新方法。

这就是代理模型继承的目的:为原始模型创建代理。你可以创建、删除和更新代理模型的实例,所有数据都将像使用原始(非代理)模型一样保存。不同之处在于,你可以更改默认模型排序或代理中的默认管理器等内容,而无需更改原始模型。

代理模型的声明方式与普通模型相同。通过将Meta类的proxy属性设置为True,来告诉 Django 它是一个代理模型。

例如,假设你想向Person模型添加一个方法。你可以这样做。

from django.db import models


class Person(models.Model):
    first_name = models.CharField(max_length=30)
    last_name = models.CharField(max_length=30)


class MyPerson(Person):
    class Meta:
        proxy = True

    def do_something(self):
        # ...
        pass

MyPerson类与它的父类Person类操作相同的数据库表。特别是,Person的任何新实例也可以通过MyPerson访问,反之亦然。

>>> p = Person.objects.create(first_name="foobar")
>>> MyPerson.objects.get(first_name="foobar")
<MyPerson: foobar>

你还可以使用代理模型在模型上定义不同的默认排序。你可能并不总是希望对Person模型进行排序,但在使用代理时,通常会按last_name属性排序。

class OrderedPerson(Person):
    class Meta:
        ordering = ["last_name"]
        proxy = True

现在,普通的Person查询将无序,而OrderedPerson查询将按last_name排序。

代理模型继承Meta属性的方式与普通模型相同

QuerySet仍然返回请求的模型

无法让 Django 返回,比如说,每当你查询Person对象时都返回一个MyPerson对象。Person对象的查询集将返回这些类型的对象。代理对象的重点是,依赖于原始Person的代码将使用这些对象,而你自己的代码可以使用你包含的扩展(无论如何其他代码都不依赖于这些扩展)。这不是用你自己的创建的东西来替换Person(或任何其他)模型的全部方法。

基类限制

代理模型必须继承自正好一个非抽象模型类。你不能从多个非抽象模型继承,因为代理模型不提供不同数据库表中行之间的任何连接。代理模型可以从任意数量的抽象模型类继承,前提是它们定义任何模型字段。代理模型还可以从任意数量的代理模型继承,这些代理模型共享一个共同的非抽象父类。

代理模型管理器

如果你没有在代理模型上指定任何模型管理器,它将继承其模型父类的管理器。如果你在代理模型上定义了一个管理器,它将成为默认管理器,尽管在父类上定义的任何管理器仍然可用。

继续我们上面的示例,你可以更改查询Person模型时使用的默认管理器,如下所示。

from django.db import models


class NewManager(models.Manager):
    # ...
    pass


class MyPerson(Person):
    objects = NewManager()

    class Meta:
        proxy = True

如果你想向代理添加一个新的管理器,而不替换现有的默认管理器,可以使用自定义管理器文档中描述的技术:创建一个包含新管理器的基类,并在主基类之后继承该基类。

# Create an abstract class for the new manager.
class ExtraManagers(models.Model):
    secondary = NewManager()

    class Meta:
        abstract = True


class MyPerson(Person, ExtraManagers):
    class Meta:
        proxy = True

你可能不需要经常这样做,但是当你需要这样做的时候,这是可能的。

代理继承和非托管模型之间的区别

代理模型继承看起来可能与创建非托管模型非常相似,使用模型Meta类上的managed属性。

通过仔细设置Meta.db_table,你可以创建一个非托管模型来隐藏现有模型并向其添加 Python 方法。但是,这将非常重复且脆弱,因为如果你进行任何更改,则需要保持两个副本同步。

另一方面,代理模型旨在与它们代理的模型行为完全一致。由于它们直接继承父模型的字段和管理器,因此它们始终与父模型同步。

一般规则如下:

  1. 如果要镜像现有的模型或数据库表,并且不需要原始数据库表的所有列,请使用 Meta.managed=False。此选项通常用于建模不受 Django 控制的数据库视图和表。

  2. 如果要更改模型的仅 Python 行为,但保留与原始模型相同的字段,请使用 Meta.proxy=True。这将设置代理模型,使其在保存数据时成为原始模型存储结构的精确副本。

多重继承

就像 Python 的子类化一样,Django 模型也可以从多个父模型继承。请记住,普通的 Python 名称解析规则适用。使用第一个出现特定名称(例如 Meta)的基类;例如,这意味着如果多个父类包含 Meta 类,则只使用第一个,其他所有类都将被忽略。

通常,不需要从多个父类继承。这有用的主要用例是“mixin”类:向继承 mixin 的每个类添加特定的额外字段或方法。尽量使继承层次结构保持简单明了,这样就不必费力地找出特定信息来自哪里。

请注意,从具有公共 id 主键字段的多个模型继承将引发错误。为了正确使用多重继承,可以在基模型中使用显式的 AutoField

class Article(models.Model):
    article_id = models.AutoField(primary_key=True)
    ...


class Book(models.Model):
    book_id = models.AutoField(primary_key=True)
    ...


class BookReview(Book, Article):
    pass

或者使用一个共同的祖先来保存 AutoField。这需要从每个父模型到公共祖先使用显式的 OneToOneField,以避免自动生成的字段和子类继承的字段之间发生冲突。

class Piece(models.Model):
    pass


class Article(Piece):
    article_piece = models.OneToOneField(
        Piece, on_delete=models.CASCADE, parent_link=True
    )
    ...


class Book(Piece):
    book_piece = models.OneToOneField(Piece, on_delete=models.CASCADE, parent_link=True)
    ...


class BookReview(Book, Article):
    pass

不允许字段名称“隐藏”

在普通的 Python 类继承中,子类可以覆盖父类的任何属性。在 Django 中,通常不允许对模型字段执行此操作。如果非抽象模型基类具有名为 author 的字段,则不能在继承自该基类的任何类中创建另一个模型字段或定义名为 author 的属性。

此限制不适用于从抽象模型继承的模型字段。可以使用另一个字段或值覆盖此类字段,或者通过设置 field_name = None 来删除它们。

警告

模型管理器继承自抽象基类。覆盖继承的 Manager 引用的继承字段可能会导致细微的错误。请参见 自定义管理器和模型继承

注意

某些字段在模型上定义额外属性,例如 ForeignKey 定义一个额外属性,其名称附加了 _id,以及在外部模型上的 related_namerelated_query_name

除非更改或删除定义它的字段,使其不再定义额外属性,否则无法覆盖这些额外属性。

覆盖父模型中的字段会导致在初始化新实例(在 Model.__init__ 中指定正在初始化的字段)和序列化等方面出现困难。这些是普通的 Python 类继承不需要以完全相同的方式处理的功能,因此 Django 模型继承与 Python 类继承之间的区别并非随意。

此限制仅适用于 Field 实例的属性。如果需要,可以覆盖普通的 Python 属性。它也只适用于 Python 看到的属性名称:如果手动指定数据库列名,则可以在子模型和祖先模型中同时出现相同的列名,用于多表继承(它们是两个不同数据库表中的列)。

如果覆盖任何祖先模型中的任何模型字段,Django 将引发 FieldError

请注意,由于在类定义期间解析字段的方式,从多个抽象父模型继承的模型字段将以严格的深度优先顺序解析。这与标准 Python MRO 形成对比,后者在菱形继承的情况下以广度优先顺序解析。此差异仅影响复杂的模型层次结构,根据上述建议,应尽量避免这种情况。

在包中组织模型

manage.py startapp 命令创建一个应用程序结构,其中包含 models.py 文件。如果有很多模型,则将它们组织到单独的文件中可能很有用。

为此,请创建一个 models 包。删除 models.py 并创建一个 myapp/models/ 目录,其中包含 __init__.py 文件以及用于存储模型的文件。必须在 __init__.py 文件中导入模型。

例如,如果在 models 目录中包含 organic.pysynthetic.py

myapp/models/__init__.py
from .organic import Person
from .synthetic import Robot

显式导入每个模型而不是使用 from .models import * 的优点是不使命名空间混乱,使代码更易于阅读,并保持代码分析工具的有用性。

另请参见

模型参考

涵盖所有与模型相关的 API,包括模型字段、相关对象和 QuerySet

返回顶部