Python 使用 Django 创建web应用(进阶-模型)

模型

模型准确且唯一的描述了数据。它包含您储存的数据的重要字段和行为。一般来说,每一个模型都映射一张数据库表。

基础:

  • 每个模型都是一个 Python 的类,这些类继承 django.db.models.Model
  • 模型类的每个属性都相当于一个数据库的字段。
  • 利用这些,Django 提供了一个自动生成访问数据库的 API;请参阅 执行查询

快速上手

这个样例定义了一个 Person 模型,拥有 first_name 和 last_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_name 和 last_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 字段会被自动添加,但是这种行为可以被改写。请参阅 自动设置主键
  • 本例子中 创建数据表 的语法是 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 冲突的名称, 如 cleansave, or delete 等.

举例:

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 值元组的 sequence,一个 mapping,一个 枚举类型,或者一个可调用对象(不接受参数并返回前述任意格式之一),用作此字段的选择项。如果提供了此参数,则默认的表单小部件将是选择框,而不是标准文本字段,并且将限制选择范围为给定的选择项。

一个选项列表:

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

备注
每当 choices 的顺序变动时将会创建新的迁移。

每个二元组的第一个值会储存在数据库中,而第二个值将只会用于在表单中显示。

对于一个模型实例,要获取该字段二元组中相对应的第二个值,使用 get_FOO_display() 方法。例如:

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)

model field reference 中定义了更多的示例。

Changed in Django 5.0:
添加了对映射和可调用对象的支持。


default:

该字段的默认值。可以是一个值或者是个可调用的对象,如果是个可调用对象,每次实例化模型时都会调用该对象。


help_text:

额外的“帮助”文本,随表单控件一同显示。即便你的字段未用于表单,它对于生成文档也是很有用的。


primary_key:

如果设置为 True ,将该字段设置为该模型的主键。

在一个模型中,如果你没有对任何一个字段设置 primary_key=True 选项。 Django 会自动添加一个 IntegerField 字段,并设置为主键,因此除非你想重写 Django 默认的主键设置行为,你可以不手动设置主键。详情请见 自动设置主键 。

主键字段是只可读的,如果你修改一个模型实例的主键并保存,这等同于创建了一个新的模型实例。例如:

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 的字段(无论是显式的设置还是 Django 自动设置)。

字段备注名

除了 ForeignKey, ManyToManyField 和 OneToOneField,任何字段类型都接收一个可选的位置参数 verbose_name,如果未指定该参数值, 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)

ForeignKeyManyToManyField and OneToOneField 接收的第一个参数为模型的类名,后面可以添加一个 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 制造许多辆车,但是每辆车都仅有一个制造者-- 那么使用下面的方法定义这个关系:

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,
    )
    # ...

参见
1.ForeignKey 字段还可以接收一些其他的参数,详见 模型字段参考 ,这些可选的参数可以更深入的规定关联关系的具体实现。
2.关于反向关联对象的细节,参见 反向关联例子
3.如要查看相关示例代码,详见 模型多对一关联实例 。

多对多关联

定义一个多对多的关联关系,使用 django.db.models.ManyToManyField 类。就和其他 Field 字段类型一样,只需要在你模型中添加一个值为该类的属性。

ManyToManyField 类需要添加一个位置参数,即你想要关联的模型类名。

例如:如果 Pizza 含有多种 Topping (配料) – 也就是一种 Topping 可能存在于多个 Pizza 中,并且每个 Pizza 含有多种 Topping --那么可以这样表示这种关系:

from django.db import models

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

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

和 ForeignKey 类一样,你也可以创建 自关联关系 (一个对象与他本身有着多对多的关系)和 与未定义的模型的关系 。

建议设置 ManyToManyField 字段名(上例中的 toppings )为一个复数名词,表示所要关联的模型对象的集合。

对于多对多关联关系的两个模型,可以在任何一个模型中添加 ManyToManyField 字段,但只能选择一个模型设置该字段,即不能同时在两模型中添加该字段。

一般来讲,应该把 ManyToManyField 实例放到需要在表单中被编辑的对象中。在之前的例子中, toppings 被放在 Pizza 当中(而不是 Topping 中有指向 pizzas 的 ManyToManyField 实例 )因为相较于配料被放在不同的披萨当中,披萨当中有很多种配料更加符合常理。按照先前说的,在编辑 Pizza 的表单时用户可以选择多种配料。

参见
如要查看完整示例代码,详见 模型多对多关联实例

ManyToManyField 字段也接受一些 模型字段参考 中介绍的参数。这些可选的参数可以更深入地规定关联关系的具体实现。

在多对多(many-to-many)关系中添加添加额外的属性字段

如果你只是想要一个类似于记录披萨和配料之间混合和搭配的多对多关系,标准的 ManyToManyField 就足够你用了。但是,有时你可能需要将数据与两个模型之间的关系相关联。

举例来讲,考虑一个需要跟踪音乐人属于哪个音乐组的应用程序。在人和他们所在的组之间有一个多对多关系,你可以使用 ManyToManyField 来代表这个关系。然而,你想要记录更多的信息在这样的关联关系当中,比如你想要记录某人是何时加入一个组的。

对于这些情况,Django 允许你指定用于控制多对多关系的模型。你可以在中间模型当中添加额外的字段。在实例化 ManyToManyField 的时候使用 through 参数指定多对多关系使用哪个中间模型。对于我们举的音乐家的例子,代码如下:

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 参数在多个外键当中手动选择一个外键,如果有多个外健且没有用 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>]>

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

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

你可能更倾向直接创建中间模型。

如果由中介模型定义的自定义中介表不对 (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 一样:在模型属性中包含它。

当一个对象以某种方式“继承”另一个对象时,这对该对象的主键非常有用。

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

例如,当你要建立一个有关“位置”信息的数据库时,你可能会包含通常的地址,电话等字段。接着,如果你想接着建立一个关于关于餐厅的数据库,除了将位置数据库当中的字段复制到 Restaurant 模型,你也可以将一个指向 Place OneToOneField 放到 Restaurant 当中(因为餐厅“是一个”地点);事实上,在处理这样的情况时最好使用 模型继承 ,它隐含的包括了一个一对一关系。

和 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!
  1. 一个字段名称不能包含连续的多个下划线,原因在于 Django 查询语法的工作方式。比如:
class Example(models.Model):
        foo__bar = models.IntegerField()  # 'foo__bar' has two underscores!
  1. 字段名不能以下划线结尾,原因同上。

但是,这些限制是可以被解决的,因为字段名没要求和数据库列名一样。查看 db_column 选项。

SQL保留字,例如 join, where 或 select,  可以被用在模型字段名当中的,因为 Django 在对底层的 SQL 查询当中清洗了所有的数据库表名和字段名,通过使用特定数据库引擎的引用语法。

自定义的字段类型

如果已经存在的模型字段不能满足你的需求,或者你希望支持一些不太常见的数据库列类型,你可以创建自己的字段类。在 编写自定义模型字段 中提供了创建自定义字段的各方面内容。

Meta 选项

使用内部 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_name 和 verbose_name_plural )。这些都不是必须的,并且在模型当中添加 Meta类 也完全是可选的。

在 模型可选参数参考 中列出了 Meta 可使用的全部选项。

模型属性

objects:

模型当中最重要的属性是 Manager。它是 Django 模型和数据库查询操作之间的接口,并且它被用作从数据库当中 获取实例,如果没有指定自定义的 Manager 默认名称是 objects。Manager 只能通过模型类来访问,不能通过模型实例来访问。

模型方法

在模型中添加自定义方法会给你的对象提供自定义的“行级”操作能力。与之对应的是类 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}"

例子中最后一个方法是 property

文档 模型实例参考 包含了 自动提供给每个模型的方法 的完整列表。你可以复写大部分的方法,参考下面的 overriding predefined model methods ——下面介绍两个你最可能期望复写的:


__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, *args, **kwargs):
        do_something()
        super().save(*args, **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, *args, **kwargs):
        if self.name == "Yoko Ono's blog":
            return  # Yoko shall never have her own blog!
        else:
            super().save(*args, **kwargs)  # Call the "real" save() method.

调用父类的方法非常重要——这里指 super().save(*args, **kwargs) ——确保对象正确的写入数据库。若你忘记调用父类方法,默认行为不会被触发,数据库也不会被操作。

同时传递模型方法接受的参数也很重要—— *args, **kwargs 会接受这些参数。Django 会不时地扩展模型内置方法的功能,也会添加新参数。如果你在重写的方法中使用了 *args, **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, force_insert=False, force_update=False, using=None, update_fields=None
    ):
        self.slug = slugify(self.name)
        if update_fields is not None and "name" in update_fields:
            update_fields = {"slug"}.union(update_fields)
        super().save(
            force_insert=force_insert,
            force_update=force_update,
            using=using,
            update_fields=update_fields,
        )

更多详细信息请参阅 指定要保存的字段

重写的模型方法不会在批量操作中调用

注意,删除一个模型对象不总是要调用 delete() 方法。例如, ref:使用 QuerySet 批量删除对象 和 级联删除。为了确保自定义的删除逻辑被执行,你可以使用 pre_delete 和 post_delete 信号。

不幸的是,批量 creating 和 updating 操作不支持上述操作,因为这两种操作未调用 save()pre_save 和 post_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 模型拥有3个字段: name, age 和 home_group。 CommonInfo 模型不能用作普通的 Django 模型,因为它是一个抽象基类。它不会生成数据表,也没有管理器,也不能被实例化和保存。

从抽象基类继承来的字段可被其它字段或值重写,或用 None 删除。

对很多用户来说,这种继承可能就是你想要的。它提供了一种在 Python 级抽出公共信息的方法,但仍会在子类模型中创建数据表。

Meta 继承

当一个抽象基类被建立,Django 将所有你在基类中申明的 Meta 内部类以属性的形式提供。若子类未定义自己的 Meta 类,它会继承父类的 Meta。当然,子类也可继承父类的 Meta,比如:

from django.db import models

class Base(models.Model):
    m2m = models.ManyToManyField(
        OtherModel,
        related_name="%(app_label)s_%(class)s_related",
        related_query_name="%(app_label)s_%(class)ss",
    )

    class Meta:
        abstract = True

class ChildA(Base):
    pass

class ChildB(Base):
    pass

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
对 related_name 和 related_query_name 要格外小心

若你在 外键 或 多对多字段 使用了 related_name 或 related_query_name,你必须为该字段提供一个 独一无二 的反向名字和查询名字。这在抽象基类中一般会引发问题,因为基类中的字段都被子类继承,且保持了同样的值(包括 related_name 和 related_query_name)。

为了解决此问题,当你在抽象基类中(也只能是在抽象基类中)使用 related_name 和 related_query_name,部分值需要包含 '%(app_label)s' 和 '%(class)s'

  • '%(class)s' 用使用了该字段的子类的小写类名替换。
  • '%(app_label)s' 用小写的包含子类的应用名替换。每个安装的应用名必须是唯一的,应用内的每个模型类名也必须是唯一的。因此,替换后的名字也是唯一的。

举个例子,有个应用 common/models.py:

from django.db import models

class Base(models.Model):
    m2m = models.ManyToManyField(
        OtherModel,
        related_name="%(app_label)s_%(class)s_related",
        related_query_name="%(app_label)s_%(class)ss",
    )

    class Meta:
        abstract = True

class ChildA(Base):
    pass

class ChildB(Base):
    pass

附带另一个应用 rare/models.py:

from common.models import Base

class ChildB(Base):
    pass

common.ChildA.m2m 字段的反转名是 common_childa_related,反转查询名是 common_childas。 common.ChildB.m2m 字段的反转名是 common_childb_related, 反转查询名是 common_childbs。 rare.ChildB.m2m 字段的反转名是 rare_childb_related,反转查询名是 rare_childbs。这决定于你如何使用 '%(class)s' 和 '%(app_label)s' 构建关联名字和关联查询名。但是,若你忘了使用它们,Django 会在你执行系统检查(或运行 migrate)时抛出错误。

如果你未指定抽象基类中的 related_name 属性,默认的反转名会是子类名,后接 '_set' 。这名字看起来就像你在子类中定义的一样。比如,在上述代码中,若省略了 related_name 属性, ChildA 的 m2m 字段的反转名会是 childa_set , ChildB 的是 childb_set

多表继承

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 异常。

Restaurant 中自动创建的连接至 Place 的 OneToOneField 看起来像这样:

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 连接子类和父类,所以直接从父类访问子类是可能的,就像上述例子展示的那样。然而,使用的名字是 ForeignKey 和 ManyToManyField 关系的默认值。如果你在继承父类模型的子类中添加了这些关联,你 必须 指定 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')

指定父类连接字段

如上所述,Django 会自动创建一个 OneToOneField ,将子类连接回非抽象的父类。如果你想修改连接回父类的属性名,你可以自己创建 OneToOneField,并设置 parent_link=True,表明该属性用于连接回父类。

代理模型

使用 多表继承 时,每个子类模型都会创建一张新表。这一般是期望的行为,因为子类需要一个地方存储基类中不存在的额外数据字段。不过,有时候你只想修改模型的 Python 级行为——可能是修改默认管理器,或添加一个方法。

这是代理模型继承的目的:为原模型创建一个 代理。你可以创建,删除和更新代理模型的实例,所以的数据都会存储的像你使用原模型(未代理的)一样。不同点是你可以修改代理默认的模型排序和默认管理器,而不需要修改原模型。

代理模型就像普通模型一样申明。你需要告诉 Django 这是一个代理模型,通过将 Meta 类的 proxy 属性设置为 True

例如,假设你想为 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 查询结果不会被排序,但 OrderdPerson 查询接轨会按 last_name 排序。

代理模型继承“Meta”属性 和普通模型一样

QuerySet 仍会返回请求的模型

当你用 Person 对象查询时,Django 永远不会返回 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,只有第一个会被使用,其它的都会被忽略。

一般来说,你并不会同时继承多个父类。常见的应用场景是 “混合” 类:为每个继承此类的添加额外的字段或方法。试着保持你的继承层级尽可能的简单和直接,这样未来你就不用为了确认某段信息是哪来的而拔你为数不多的头发了。

注意,继承自多个包含 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_name 和 related_query_name

这些额外的属性不能被覆盖,除非定义它的字段被改变或删除,使它不再定义额外的属性。

重写父模型中的字段会导致一些困难,比如初始化新实例(在 Model.__init__ 中指定哪个字段被初始化)和序列化。这些都是普通的 Python 类继承所不需要处理的功能,所以 Django 模型继承和 Python 类继承之间的区别并不是任意的。

这些限制只针对那些是 Field 实例的属性。普通的 Python 属性可被随便重写。它还对 Python 能识别的属性生效:如果你同时在子类和多表继承的祖先类中指定了数据表的列名(它们是两张不同的数据表中的列)。

若你在祖先模型中重写了任何模型字段,Django 会抛出一个 FieldError

请注意,由于字段在类定义期间的解析方式,从多个抽象父模型继承的模型字段将按照严格的深度优先顺序解析。这与标准的 Python MRO(方法解析顺序)不同,后者在出现菱形继承的情况下以广度优先方式解析。这个差异只会影响复杂的模型层次结构,根据上面的建议,你应该尽量避免使用复杂的模型层次结构。

在一个包中管理模型

manage.py startapp 命令创建了一个应用结构,包含一个 models.py 文件。若你有很多 models.py 文件,用独立的文件管理它们会很实用。

为了达到此目的,创建一个 models 包。删除 models.py,创建一个 myapp/models 目录,包含一个 __init__.py 文件和存储模型的文件。你必须在 __init__.py 文件中导入这些模块。

比如,若你在 models 目录下有 organic.py 和 synthetic.py

myapp/models/__init__.py

from .organic import Person
from .synthetic import Robot

显式导入每个模块,而不是使用 from .models import * 有助于不打乱命名空间,使代码更具可读性,让代码分析工具更有用。

参见
模型参考
覆盖所有的模型关联 API,包括模型字段,关联对象和 QuerySet

  • 5
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
### 回答1: 《精通 Django 3 Web 开发》是一本非常优秀的Python Web 开发图书,它全面介绍了 Django 框架的使用方法,以及如何在实际项目中运用 Django 构建高可用、高性能的 Web 应用程序。 本书内容详实、全面,涵盖了 Django 3 的方方面面,从 Django 的基础语法和模型设计,到数据库的使用、视图、模板、表单、信号和很多实际的开发技巧等,都有非常详细的讲解,适合 Django 的初学者和进阶开发者阅读。 另外,本书还特别介绍了 Django Rest Framework (DRF) 的使用方法,该框架是 Django 的一个扩展,用于构建 RESTful API,它非常适合构建 Web 服务和前后端分离的应用。作者详细介绍了 DRF 的各种功能,包括序列化、视图、认证、权限、过滤器、分页等,让读者快速掌握 DRF 的使用。 总的来说,《精通 Django 3 Web 开发》这本书涵盖了 Django 3 开发的方方面面,是一本非常优秀的 Django 开发指南,对于想系统学习 Django 开发的人来说也是一本非常好的教材。值得一提的是,本书还提供了很多实战项目,使读者能够更好地吸收 Django 的开发技能,深入了解 Django 的各种应用场景。如果你是 Python Web 开发爱好者,强烈推荐你阅读这本书。 ### 回答2: 精通Django 3 Web开发PDF是一本全面介绍Django Web开发框架的书籍。Django是一个用于构建Web应用程序的Python框架。Django提供了强大的数据库ORM和快速开发工具,使得开发人员可以轻松地创建功能强大的Web应用程序。这本书介绍了Django 3的所有核心功能,包括视图、表单、模板、数据库操作、用户认证等。此外,它还介绍了Django的安全性能和一些高级主题,如缓存和性能优化。 通过学习本书中的内容,读者可以深入了解Django的开发方式,并在实践中构建自己的Web应用程序。无论是初学者还是有经验的开发人员,都可以从这本书中获得更多知识和理解。总之,如果你想学习Django Web开发框架,精通Django 3 Web开发PDF是一个非常好的学习资源,它可以帮助你快速入门并快速实现Web应用程序。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

qq_34037510

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值