内容类型框架Link to this heading

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

概况Link to this heading

内容类型应用的核心是 ContentType 模型,它位于 django.contrib.contenttypes.models.ContentTypeContentType 的实例代表和存储了你项目中安装的模型的信息,每当有新的模型安装时,就会自动创建 ContentType 的新实例。

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

你的模型和 ContentType 之间的关系也可以用来启用你的一个模型实例和你安装的任何模型实例之间的“通用 ”关系。

安装内容类型框架Link to this heading

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

一般来说,安装内容类型框架是个不错的主意;Django 的其他一些捆绑的应用程序都需要它:

  • 管理应用程序使用它来记录通过管理界面添加或更改的每个对象的历史。

  • Django 的 认证框架 使用它将用户权限与特定模型绑定。

ContentType 模型Link to this heading

class ContentTypeLink to this definition

ContentType 的每个实例都有两个字段,这两个字段合在一起,唯一地描述了一个安装的模型。

app_labelLink to this definition

模型所属应用程序的名称。这是从模型的 app_label 属性中提取的,并且只包括应用程序的 Python 导入路径的 最后 一部分;例如,django.contrib.contenttypes 就变成了 contenttypesapp_label

modelLink to this definition

模型类的名称。

此外,还有以下属性:

nameLink to this definition

内容类型的可读名称。这是从模型的 verbose_name 属性中提取的。

让我们看一个例子来了解它是如何工作的。如果你已经安装了 contenttypes 应用程序,然后添加 站点框架 到你的 INSTALLED_APPS 配置中,并运行 manage.py migrate 来安装它,模型 django.contrib.sites.models.Site 将被安装到你的数据库中。与它一起创建一个新的 ContentType 实例,其值如下:

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

  • model 将被设置为 'site'

ContentType 实例的方法Link to this heading

每个 ContentType 实例都有一些方法,允许你从 ContentType 实例获得它所代表的模型,或者从该模型中检索对象。

ContentType.get_object_for_this_type(using=None, **kwargs)Link to this definition

接受一组有效的 查找参数,用于 ContentType 表示的模型,并在该模型上执行 get() 查找,返回相应的对象。using 参数可用于指定不同于默认数据库的数据库。

ContentType.model_class()Link to this definition

返回这个 ContentType 实例所代表的模型类。

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

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

然后可以使用它来查询特定的 User,或者访问 User 模型类:

Python console
>>> 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 的认证框架中的 :class:``权限系统 <django.contrib.auth.models.Permission>` 使用了一个 Permission 模型,该模型的外键为 ContentType;这使得 Permission 可以表示“可以添加博客条目”或“可以删除新闻报道”等概念。

ContentTypeManagerLink to this heading

class ContentTypeManagerLink to this definition

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

clear_cache()Link to this definition

Clears an internal cache used by ContentType to keep track of models for which it has created ContentType instances. You probably won't need to call this method in application code yourself; Django will call it automatically when it's needed.

You may need to clear the cache when testing, to reset between tests, or after preparing test state. For example:

Code
class ContentTypesTests(TestCase):
    def setUp(self):
        ContentType.objects.clear_cache()
        self.addCleanup(ContentType.objects.clear_cache)
get_for_id(id)Link to this definition

通过 ID 查找一个 ContentType。由于该方法与 get_for_model() 使用了相同的共享缓存,所以最好使用该方法,而不是通常的 ContentType.objects.get(pk=id)

get_for_model(model, for_concrete_model=True)Link to this definition

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

get_for_models(*models, for_concrete_models=True)Link to this definition

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

get_by_natural_key(app_label, model)Link to this definition

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

The get_for_model() method is especially useful when you know you need to work with a ContentType but don't want to go to the trouble of obtaining the model's metadata to perform a manual lookup:

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

通用关系Link to this heading

ContentType 中添加一个来自你自己模型的外键,可以让你的模型有效地将自己与另一个模型类绑定,就像上面 Permission 模型的例子一样。但也可以更进一步,使用 ContentType 来实现模型之间真正的通用(有时也称为 “多态”)关系。

例如,它可以用于这样的标签系统:

Code
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.PositiveBigIntegerField()
    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 GenericForeignKeyLink to this definition

设置一个 GenericForeignKey 分为三步:

  1. 给你的模型一个 ForeignKeyContentType。这个字段的通常名称是 “content_type”。

  2. Give your model a field that can store primary key values from the models you'll be relating to. For most models, this means a PositiveBigIntegerField. The usual name for this field is "object_id".

  3. 给你的模型一个 GenericForeignKey,并把上面描述的两个字段的名字传给它。如果这些字段的名字是 “content_type” 和 “object_id”,你可以省略这一点 —— 这些是 GenericForeignKey 会查找的默认字段名。

ForeignKey 不同,对于 GenericForeignKey 并不会自动创建数据库索引,因此建议您使用 Meta.indexes 来添加自己的多列索引。这种行为在将来 可能会发生变化

for_concrete_modelLink to this definition

如果 False,该字段将能够引用代理模型。默认值是 True。这与 get_for_model()for_concrete_model 参数一致。

这将启用类似于正常的 ForeignKey 使用的 API;每个 TaggedItem 将具有一个 content_object 字段,该字段返回与其关联的对象,您还可以分配给该字段或在创建 TaggedItem 时使用它:

Python console
>>> 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

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

由于 GenericForeignKey 的实现方式,您不能直接使用这些字段通过数据库 API 进行筛选(例如,filter()exclude())。因为 GenericForeignKey 不是一个普通的字段对象,所以这些示例 不会 生效:

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

Likewise, GenericForeignKeys do not appear in ModelForms.

反查通用关系Link to this heading

class GenericRelationLink to this definition
related_query_nameLink to this definition

默认情况下,相关对象与本对象的关系并不存在。设置 related_query_name 创建一个从相关对象到这个对象的关系。这样就可以从关联对象中进行查询和过滤。

如果你知道哪些模型你会最经常使用,你也可以添加一个 “反向” 的通用关系来启用一个额外的 API。例如:

Code
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

Python console
>>> b = Bookmark(url="https://www.djangoproject.com/")
>>> 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() 来创建关系:

Python console
>>> 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() 调用将批量删除指定的模型对象:

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

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

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

定义 GenericRelation,并设置 related_query_name 允许从相关对象查询:

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

这使得可以从 TaggedItemBookmark 进行过滤、排序和其他查询操作:

Python console
>>> # 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,您可以手动执行相同类型的查找操作:

Python console
>>> 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 接受content-type 和 object-ID 字段的名称作为参数一样, GenericRelation 也是如此;如果拥有通用外键的模型对这些字段使用了非默认的名称,那么在给它设置 GenericRelation 时必须传递这些字段的名称。例如,如果上面提到的 TaggedItem 模型使用了名为 content_type_fkobject_primary_key 的字段来创建它的通用外键,那么回传给它的 GenericRelation 就需要这样定义:

Code
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 信号来提供替代行为。

通用关系和聚合Link to this heading

Django 的数据库聚合 API 可以与 GenericRelation 一起使用。例如,您可以找出所有书签有多少个标签:

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

表单中的通用关系Link to this heading

django.contrib.contenttypes.forms 模块提供:

class BaseGenericInlineFormSetLink to this definition
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)Link to this definition

使用 modelormset_factory() 返回一个 GenericInlineFormSet

你必须提供 ct_fieldfk_field,如果它们与默认值 content_typeobject_id 不同。其他参数与 modelselformset_factory()inlineformset_factory() 中记载的类似。

for_concrete_model 参数对应于 for_concrete_model` 参数。

管理中的通用关系Link to this heading

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

这些类和函数可以在表单和管理中使用通用关系。更多信息请参见 模型表单集管理 文档。

class GenericInlineModelAdminLink to this definition

GenericInlineModelAdmin 类继承了 InlineModelAdmin 类的所有属性。然而,它增加了一些自己的属性来处理通用关系:

ct_fieldLink to this definition

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

ct_fk_fieldLink to this definition

代表相关对象 ID 的整数字段的名称。默认值为 object_id

class GenericTabularInlineLink to this definition
class GenericStackedInlineLink to this definition

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

GenericPrefetch()Link to this heading

class GenericPrefetch(lookup, querysets, to_attr=None)Link to this definition

此查找类似于 Prefetch(),应仅用于 GenericForeignKeyquerysets 参数接受一个查询集列表,每个查询集对应不同的 ContentType。这对于具有非同质结果集的 GenericForeignKey 非常有用。

Python console
>>> from django.contrib.contenttypes.prefetch import GenericPrefetch
>>> bookmark = Bookmark.objects.create(url="https://www.djangoproject.com/")
>>> 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>]>