内容类型框架¶
Django 包含了一个 contenttypes 应用程序,它可以跟踪所有安装在你的 Django 项目中的模型,为你的模型提供了一个高级的通用接口。
概况¶
内容类型应用的核心是 ContentType 模型,它位于 django.contrib.contenttypes.models.ContentType。ContentType 的实例代表和存储了你项目中安装的模型的信息,每当有新的模型安装时,就会自动创建 ContentType 的新实例。
ContentType 的实例有方法用于返回它们所代表的模型类和查询这些模型中的对象。 ContentType 也有一个 自定义管理器,它增加了一些方法,用于处理 ContentType,以及为特定模型获取 ContentType 的实例。
你的模型和 ContentType 之间的关系也可以用来启用你的一个模型实例和你安装的任何模型实例之间的“通用 ”关系。
安装内容类型框架¶
内容类型框架包含在由 django-admin startproject 创建的默认的 INSTALLED_APPS 列表中,但是如果你已经删除了它,或者你手动设置了 INSTALLED_APPS 列表,你可以通过在 INSTALLED_APPS 配置中添加 'django.contrib.contenttypes' 来启用它。
一般来说,安装内容类型框架是个不错的主意;Django 的其他一些捆绑的应用程序都需要它:
- 管理应用程序使用它来记录通过管理界面添加或更改的每个对象的历史。
- Django 的
认证框架使用它将用户权限与特定模型绑定。
ContentType 模型¶
-
class
ContentType¶ ContentType的每个实例都有两个字段,这两个字段合在一起,唯一地描述了一个安装的模型。-
app_label¶ 模型所属应用程序的名称。这是从模型的
app_label属性中提取的,并且只包括应用程序的 Python 导入路径的 最后 一部分;例如,django.contrib.contenttypes就变成了contenttypes的app_label。
-
model¶ 模型类的名称。
此外,还有以下属性:
-
name¶ 内容类型的可读名称。这是从模型的
verbose_name属性中提取的。
-
让我们看一个例子来了解它是如何工作的。如果你已经安装了 contenttypes 应用程序,然后添加 站点框架 到你的 INSTALLED_APPS 配置中,并运行 manage.py migrate 来安装它,模型 django.contrib.sites.models.Site 将被安装到你的数据库中。与它一起创建一个新的 ContentType 实例,其值如下:
ContentType 实例的方法¶
每个 ContentType 实例都有一些方法,允许你从 ContentType 实例获得它所代表的模型,或者从该模型中检索对象。
-
ContentType.get_object_for_this_type(**kwargs)¶ 为
ContentType所代表的模型获取一组有效的 查找参数,并对该模型进行一个 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_model_class() 共同实现了两个极其重要的用例:
- 使用这些方法,你可以编写高级通用代码,在任何安装的模型上执行查询——而不是导入和使用一个特定的模型类,你可以在运行时将
app_label和model传递到一个ContentType的查找中,然后与模型类一起工作,或者从中检索对象。 - 你可以将另一个模型与
ContentType相关联,以此将它的实例与特定的模型类绑定,并使用这些方法来获取对这些模型类的访问。
Django 的几个捆绑应用都使用了后一种技术。例如,Django 的认证框架中的 :class:``权限系统 <django.contrib.auth.models.Permission>` 使用了一个 Permission 模型,该模型的外键为 ContentType;这使得 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分为三步:- 给你的模型一个
ForeignKey到ContentType。这个字段的通常名称是 “content_type”。 - 给你的模型一个字段,它可以存储你要关联的模型的主键值。对于大多数模型来说,这意味着一个
PositiveIntegerField。这个字段的通常名称是 “object_id”。 - 给你的模型一个
GenericForeignKey,并把上面描述的两个字段的名字传给它。如果这些字段的名字是 “content_type” 和 “object_id”,你可以省略这一点 —— 这些是GenericForeignKey会查找的默认字段名。
Unlike for the
ForeignKey, a database index is not automatically created on theGenericForeignKey, so it's recommended that you useMeta.indexesto add your own multiple column index. This behavior may change in the future.-
for_concrete_model¶ 如果
False,该字段将能够引用代理模型。默认值是True。这与get_for_model()的for_concrete_model参数一致。
- 给你的模型一个
主键类型兼容性
“object_id” 字段不一定要和相关模型上的主键字段是同一类型,但它们的主键值必须通过其 get_db_prep_value() 方法与 “object_id” 字段的类型一致。
例如,如果你想允许通用关系到具有 CharField 主键字段的模型,你可以使用 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_type 和 object_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创建一个从相关对象到这个对象的关系。这样就可以从关联对象中进行查询和过滤。
如果你知道哪些模型你会最经常使用,你也可以添加一个 “反向” 的通用关系来启用一个额外的 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.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() 来创建关系:
>>> 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 []>
定义 GenericRelation,并设置 related_query_name 允许从相关对象查询:
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 接受content-type 和 object-ID 字段的名称作为参数一样, GenericRelation 也是如此;如果拥有通用外键的模型对这些字段使用了非默认的名称,那么在给它设置 GenericRelation 时必须传递这些字段的名称。例如,如果上面提到的 TaggedItem 模型使用了名为 content_type_fk 和 object_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 的数据库聚合 API 的工作原理是 GenericRelation。例如,你可以找出所有书签有多少个标签:
>>> 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)¶ 使用
modelormset_factory()返回一个GenericInlineFormSet。你必须提供
ct_field和fk_field,如果它们与默认值content_type和object_id不同。其他参数与modelselformset_factory()和inlineformset_factory()中记载的类似。for_concrete_model参数对应于for_concrete_model`参数。Changed in Django 3.2:增加了
absolute_max和can_delete_extra参数。
管理中的通用关系¶
django.contrib.contenttypes.admin 模块提供了 GenericTabularInline 和 GenericInlineModelAdmin 的子类)。
这些类和函数可以在表单和管理中使用通用关系。更多信息请参见 模型表单集 和 管理 文档。
-
class
GenericInlineModelAdmin¶ GenericInlineModelAdmin类继承了InlineModelAdmin类的所有属性。然而,它增加了一些自己的属性来处理通用关系:-
ct_field¶ 模型上的
ContentType外键字段的名称。默认为content_type。
-
ct_fk_field¶ 代表相关对象 ID 的整数字段的名称。默认值为
object_id。
-
-
class
GenericTabularInline¶
-
class
GenericStackedInline¶ GenericInlineModelAdmin的子类,分别具有堆栈式和表格式布局。