contenttypes 框架

Django 包含一个 contenttypes 应用程序,它可以跟踪在由 Django 提供支持的项目中安装的所有模型,为使用模型提供高级通用接口。

概览

contenttypes 应用程序的核心是 ContentType 模型,它位于 django.contrib.contenttypes.models.ContentTypeContentType 的实例表示并存储有关项目中已安装模型的信息,并且每当安装新模型时都会自动创建 ContentType 的新实例。

ContentType 的实例具有用于返回它们所表示的模型类和从这些模型中查询对象的方法。 ContentType 还具有一个 自定义管理器,该管理器添加了用于处理 ContentType 以及获取特定模型的 ContentType 实例的方法。

模型和 ContentType 之间的关系也可用于启用一个模型的实例和已安装的任何模型的实例之间的“泛型”关系。

安装内容类型框架

内容类型框架包含在 django-admin startproject 创建的默认 INSTALLED_APPS 列表中,但如果您已将其删除或手动设置了 INSTALLED_APPS 列表,则可以通过将 'django.contrib.contenttypes' 添加到 INSTALLED_APPS 设置中来启用它。

通常安装内容类型框架是一个好主意;Django 的其他几个捆绑应用程序需要它

  • 管理员应用程序使用它来记录通过管理员界面添加或更改的每个对象的记录。
  • Django 的 authentication framework 使用它将用户权限绑定到特定模型。

ContentType 模型

class ContentType

每个 ContentType 实例有两个字段,它们合在一起唯一描述了一个已安装的模型

app_label

该模型所属应用程序的名称。这是从模型的 app_label 属性中获取的,并且仅包含应用程序 Python 导入路径的最后部分;例如,django.contrib.contenttypes 成为 app_label contenttypes

model

模型类的名称。

此外,以下属性可用

name

内容类型的可读名称。此名称取自模型的 verbose_name 属性。

我们来看一个示例,了解其工作原理。如果您已安装 contenttypes 应用程序,然后将 sites 应用程序 添加到 INSTALLED_APPS 设置中,并运行 manage.py migrate 以安装它,则模型 django.contrib.sites.models.Site 将安装到您的数据库中。同时,将创建 ContentType 的新实例,其值如下

  • app_label 将设置为 'sites'(Python 路径 django.contrib.sites 的最后一部分)。
  • model 将被设置为 'site'

ContentType 实例上的方法

每个 ContentType 实例都有允许你从 ContentType 实例获取到它表示的模型,或从该模型中检索对象的方法

ContentType.get_object_for_this_type(**kwargs)

获取 ContentType 表示的模型的有效 查找参数 的集合,并在该模型上执行 a get() 查找,返回相应的对象。

ContentType.model_class()

返回此 ContentType 实例表示的模型类。

例如,我们可以查找 ContentType 以获取 User 模型

>>> from django.contrib.contenttypes.models import ContentType
>>> user_type = ContentType.objects.get(app_label="auth", model="user")
>>> user_type
<ContentType: user>

然后使用它查询特定的 User,或获取 User 模型类

>>> user_type.model_class()
<class 'django.contrib.auth.models.User'>
>>> user_type.get_object_for_this_type(username="Guido")
<User: Guido>

同时,get_object_for_this_type()model_class() 启用两个极其重要的用例

  1. 使用这些方法,您可以编写对任何已安装模型执行查询的高级通用代码 – 而不是导入和使用单个特定模型类,您可以将 app_labelmodel 传递到 ContentType 查找中,然后使用模型类或从中检索对象。
  2. 您可以将另一个模型与 ContentType 关联,作为将其实例与特定模型类绑定的一种方式,并使用这些方法来访问这些模型类。

Django 的几个捆绑应用程序使用了后一种技术。例如,Django 认证 框架 中的 权限 系统 使用带有外键到 ContentTypePermission 模型;这允许 Permission 表示诸如“可以添加博客条目”或“可以删除新闻故事”之类的概念。

ContentTypeManager

class ContentTypeManager

ContentType 还有一个自定义管理器,ContentTypeManager,它添加了以下方法

clear_cache()

清除 ContentType 用于跟踪已为其创建 ContentType 实例的模型的内部缓存。您可能永远不需要自己调用此方法;Django 会在需要时自动调用它。

get_for_id(id)

按 ID 查找 ContentType。由于此方法使用与 get_for_model() 相同的共享缓存,因此建议使用此方法,而不是通常的 ContentType.objects.get(pk=id)

get_for_model(model, for_concrete_model=True)

接受模型类或模型实例,并返回表示该模型的 ContentType 实例。 for_concrete_model=False 允许获取代理模型的 ContentType

get_for_models(*models, for_concrete_models=True)

接受可变数量的模型类,并返回一个字典,将模型类映射到表示它们的 ContentType 实例。 for_concrete_models=False 允许获取代理模型的 ContentType

get_by_natural_key(app_label, model)

返回由给定的应用程序标签和模型名称唯一标识的 ContentType 实例。此方法的主要目的是允许 ContentType 对象在反序列化期间通过 自然键 进行引用。

当您知道需要使用 ContentType 但又不想费力获取模型的元数据以执行手动查找时,get_for_model() 方法特别有用

>>> from django.contrib.auth.models import User
>>> ContentType.objects.get_for_model(User)
<ContentType: user>

泛型关系

从您自己的模型之一向 ContentType 添加外键,让您的模型能够有效地将自身绑定到另一个模型类,如上述 Permission 模型示例。但可以更进一步,使用 ContentType 来支持模型之间的真正通用(有时称为“多态”)关系。

例如,它可用于标记系统,如下所示

from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
from django.db import models


class TaggedItem(models.Model):
    tag = models.SlugField()
    content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
    object_id = models.PositiveIntegerField()
    content_object = GenericForeignKey("content_type", "object_id")

    def __str__(self):
        return self.tag

    class Meta:
        indexes = [
            models.Index(fields=["content_type", "object_id"]),
        ]

普通 ForeignKey 只能“指向”另一个模型,这意味着如果 TaggedItem 模型使用了 ForeignKey,它必须选择一个且仅一个模型来存储标签。contenttypes 应用程序提供了一种特殊字段类型 (GenericForeignKey),可以解决此问题,并允许与任何模型建立关系

class GenericForeignKey

设置 GenericForeignKey 有三个部分

  1. 为模型提供一个 ForeignKeyContentType。此字段的常用名称为“content_type”。
  2. 为模型提供一个可以存储将关联到的模型的主键值的字段。对于大多数模型,这意味着一个 PositiveIntegerField。此字段的常用名称为“object_id”。
  3. 为模型提供一个 GenericForeignKey,并向其传递上面描述的两个字段的名称。如果这些字段的名称为“content_type”和“object_id”,则可以省略此操作,这些是 GenericForeignKey 将查找的默认字段名称。

ForeignKey 不同,数据库索引不会自动在 GenericForeignKey 上创建,因此建议使用 Meta.indexes 添加自己的多列索引。此行为 可能会更改

for_concrete_model

如果 False,该字段将能够引用代理模型。默认值为 True。这反映了 get_for_model() 中的 for_concrete_model 参数。

主键类型兼容性

“object_id”字段不必与相关模型上的主键字段类型相同,但其主键值必须可通过其 get_db_prep_value() 方法强制转换为与“object_id”字段类型相同。

例如,如果您想允许具有 IntegerFieldCharField 主键字段的模型进行通用关系,则可以使用 CharField 作为模型上的“object_id”字段,因为整数可以通过 get_db_prep_value() 转换为字符串。

为了获得最大的灵活性,您可以使用未定义最大长度的 TextField,但根据您的数据库后端,这可能会造成严重的性能损失。

对于哪种字段类型最佳,没有一刀切的解决方案。您应该评估您期望指向的模型,并确定哪种解决方案对您的用例最有效。

序列化对 ContentType 对象的引用

如果您正在序列化数据(例如,在从实现通用关系的模型生成 fixtures 时),您可能应该使用自然键来唯一标识相关的 ContentType 对象。有关更多信息,请参阅 自然键dumpdata --natural-foreign

这将启用类似于用于普通 ForeignKey 的 API;每个 TaggedItem 都将有一个 content_object 字段,该字段返回与其相关的对象,您还可以在创建 TaggedItem 时将该字段分配给该字段或使用该字段。

>>> from django.contrib.auth.models import User
>>> guido = User.objects.get(username="Guido")
>>> t = TaggedItem(content_object=guido, tag="bdfl")
>>> t.save()
>>> t.content_object
<User: Guido>

如果关联对象被删除,content_typeobject_id 字段仍保留其原始值,并且 GenericForeignKey 返回 None

>>> guido.delete()
>>> t.content_object  # returns None

由于 GenericForeignKey 的实现方式,您无法通过数据库 API 直接将此类字段与过滤器(例如 filter()exclude())一起使用。因为 GenericForeignKey 不是一个常规的字段对象,所以以下示例不起作用

# This will fail
>>> TaggedItem.objects.filter(content_object=guido)
# This will also fail
>>> TaggedItem.objects.get(content_object=guido)

同样,GenericForeignKey 不会出现在 ModelForm 中。

反向泛型关系

class GenericRelation
related_query_name

默认情况下,关联对象与本对象的关联关系不存在。设置 related_query_name 会创建从关联对象返回到本对象的关联关系。这允许从关联对象进行查询和过滤。

如果您知道最常使用的模型,还可以添加“反向”泛型关系来启用其他 API。例如

from django.contrib.contenttypes.fields import GenericRelation
from django.db import models


class Bookmark(models.Model):
    url = models.URLField()
    tags = GenericRelation(TaggedItem)

Bookmark 实例将分别具有 tags 属性,该属性可用于检索其关联的 TaggedItems

>>> b = Bookmark(url="https://www.django.ac.cn/")
>>> b.save()
>>> t1 = TaggedItem(content_object=b, tag="django")
>>> t1.save()
>>> t2 = TaggedItem(content_object=b, tag="python")
>>> t2.save()
>>> b.tags.all()
<QuerySet [<TaggedItem: django>, <TaggedItem: python>]>

您还可以使用 add()create()set() 来创建关系

>>> t3 = TaggedItem(tag="Web development")
>>> b.tags.add(t3, bulk=False)
>>> b.tags.create(tag="Web framework")
<TaggedItem: Web framework>
>>> b.tags.all()
<QuerySet [<TaggedItem: django>, <TaggedItem: python>, <TaggedItem: Web development>, <TaggedItem: Web framework>]>
>>> b.tags.set([t1, t3])
>>> b.tags.all()
<QuerySet [<TaggedItem: django>, <TaggedItem: Web development>]>

remove() 调用将批量删除指定的模型对象

>>> b.tags.remove(t3)
>>> b.tags.all()
<QuerySet [<TaggedItem: django>]>
>>> TaggedItem.objects.all()
<QuerySet [<TaggedItem: django>]>

可以使用 clear() 方法批量删除实例的所有相关对象

>>> b.tags.clear()
>>> b.tags.all()
<QuerySet []>
>>> TaggedItem.objects.all()
<QuerySet []>

使用 related_query_name 设置定义 GenericRelation 允许从相关对象进行查询

tags = GenericRelation(TaggedItem, related_query_name="bookmark")

这使得能够对 TaggedItem 中的 Bookmark 进行过滤、排序和其他查询操作

>>> # Get all tags belonging to bookmarks containing `django` in the url
>>> TaggedItem.objects.filter(bookmark__url__contains="django")
<QuerySet [<TaggedItem: django>, <TaggedItem: python>]>

如果您不添加 related_query_name,则可以手动执行相同类型的查找

>>> bookmarks = Bookmark.objects.filter(url__contains="django")
>>> bookmark_type = ContentType.objects.get_for_model(Bookmark)
>>> TaggedItem.objects.filter(content_type__pk=bookmark_type.id, object_id__in=bookmarks)
<QuerySet [<TaggedItem: django>, <TaggedItem: python>]>

正如 GenericForeignKey 接受内容类型和对象 ID 字段的名称作为参数一样,GenericRelation 也是如此;如果具有泛型外键的模型对这些字段使用非默认名称,则必须在为 GenericRelation 设置时传递字段的名称。例如,如果上面提到的 TaggedItem 模型使用名为 content_type_fkobject_primary_key 的字段来创建其泛型外键,那么指向它的 GenericRelation 需要像这样定义

tags = GenericRelation(
    TaggedItem,
    content_type_field="content_type_fk",
    object_id_field="object_primary_key",
)

还要注意,如果您删除具有 GenericRelation 的对象,则指向它的任何具有 GenericForeignKey 的对象也将被删除。在上面的示例中,这意味着如果删除 Bookmark 对象,则指向它的任何 TaggedItem 对象也将同时被删除。

ForeignKey 不同,GenericForeignKey 不接受 on_delete 参数来定制此行为;如果需要,你可以不使用 GenericRelation 来避免级联删除,而可以通过 pre_delete 信号提供备选行为。

泛型关系和聚合

Django 的数据库聚合 APIGenericRelation 协同工作。例如,你可以找出所有书签有多少个标签

>>> Bookmark.objects.aggregate(Count("tags"))
{'tags__count': 3}

表单中的泛型关系

django.contrib.contenttypes.forms 模块提供

class BaseGenericInlineFormSet
generic_inlineformset_factory(model, form=ModelForm, formset=BaseGenericInlineFormSet, ct_field='content_type', fk_field='object_id', fields=None, exclude=None, extra=3, can_order=False, can_delete=True, max_num=None, formfield_callback=None, validate_max=False, for_concrete_model=True, min_num=None, validate_min=False, absolute_max=None, can_delete_extra=True)

使用 modelformset_factory() 返回 GenericInlineFormSet

如果您提供的 ct_fieldfk_field 与默认值不同,则必须提供 content_typeobject_id。其他参数与 modelformset_factory()inlineformset_factory() 中记录的参数类似。

for_concrete_model 参数对应于 GenericForeignKey 上的 for_concrete_model 参数。

管理中的泛型关系

django.contrib.contenttypes.admin 模块提供了 GenericTabularInlineGenericStackedInlineGenericInlineModelAdmin 的子类)

这些类和函数支持在表单和管理中使用泛型关系。有关更多信息,请参阅 模型表单集管理 文档。

class GenericInlineModelAdmin

GenericInlineModelAdmin 继承自类 InlineModelAdmin 的所有属性。但是,它添加了一些用于处理泛型关系的属性

ct_field

模型上 ContentType 外键字段的名称。默认为 content_type

ct_fk_field

表示相关对象 ID 的整型字段的名称。默认为 object_id

GenericTabularInline
GenericStackedInline

GenericInlineModelAdmin 的子类,分别具有堆叠和表格布局。

GenericPrefetch()

Django 5.0 中的新增功能。
GenericPrefetch(lookup, querysets, to_attr=None)

此查找类似于 Prefetch(),它只应在 GenericForeignKey 上使用。 querysets 参数接受一个查询集列表,每个列表对应一个不同的 ContentType。这对于具有非同质结果集的 GenericForeignKey 很有用。

>>> from django.contrib.contenttypes.prefetch import GenericPrefetch
>>> bookmark = Bookmark.objects.create(url="https://www.django.ac.cn/")
>>> animal = Animal.objects.create(name="lion", weight=100)
>>> TaggedItem.objects.create(tag="great", content_object=bookmark)
>>> TaggedItem.objects.create(tag="awesome", content_object=animal)
>>> prefetch = GenericPrefetch(
...     "content_object", [Bookmark.objects.all(), Animal.objects.only("name")]
... )
>>> TaggedItem.objects.prefetch_related(prefetch).all()
<QuerySet [<TaggedItem: Great>, <TaggedItem: Awesome>]>
返回顶部