Contenttypes框架

Django包含一个contenttypes应用,它可以追踪Django项目里安装的所有应用,提供一个高层次的、通用的接口用于与模型交互。

概述

Contenttypes应用的核心是ContentType模型,位于django.contrib.contenttypes.models.ContentType ContentType的实例表示并保存项目中安装的模型的信息,每当有新的模型时会自动创建新的ContentType实例。

ContentType的实例可以返回它们表示的模型类以及从这些模型查询对象。 ContentType还有一个自定义的管理器,它添加的方法可以与ContentType一起工作,用于获得特定模型的ContentType实例。

模型和ContentType之间的关系还可以用于模型的一个实例和任意一个已经安装的模型的实例建立“generic关联”。

安装Contenttypes框架

Contenttypes 框架包含在django-admin startproject 创建的默认的INSTALLED_APPS 列表中,但如果你移除了它或者你手动创建 INSTALLED_APPS 列表,你可以通过添加'django.contrib.contenttypes'到你的 INSTALLED_APPS 设置中来启用它。

安装contenttypes框架通常是个好主意;几个Django的其他捆绑应用程序需要它:

  • Admin 应用使用它来记录通过Admin 界面添加或更改每个对象的历史。
  • Django 的authentication framework用它来授用户权限给特殊的模型。

ContentType模型

class ContentType

每一个ContentType实例有两个字段,共同来唯一描述一个已经安装的模型。

app_label

模型所在的应用的名称。 这取自模型的app_label属性,仅包含应用的Python导入路径的最后一个部分;例如django.contrib.contenttypesapp_labelcontenttypes

model

模型的类的名称。

此外,还有下面的属性︰

name

Contenttype适于阅读的名称。 它取之于模型的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表示的模型所接收的查找参数,对该模型做一个get()查找,然后返回相应的对象。

ContentType.model_class()

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

例如,我们可以通过ContentType查找到User模型︰

>>> from django.contrib.contenttypes.models import ContentType
>>> ContentType.objects.get(app_label="auth", model="user")
<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_labelmodelContentType在运行时查找,然后使用这个模型类或从它获取对象。
  2. 你可以关联另一个模型到ContentType作为一种绑定它的实例到特定模型类的方式,然后使用这些方法来获取对那些模型的访问。

几个Django捆绑的应用就利用到后者这个技术。 例如,在Django身份验证框架中,权限系统使用的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 对象在反序列化期间通过natural key来引用。

get_for_model() 方法特别有用,当你知道你需要与ContentType 交互但不想要去获取模型元数据以执行手动查找的麻烦︰

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

通用关系

在你的model添加一个外键到 ContentType这将允许你更快捷的绑定自身到其他的model class,就像上述的 Permission model 一样。 但是它非常有可能进一步的利用 ContentType 来实现真正的 generic (有时称之为多态) relationships 在models之间。

一个简单的例子是标记系统,它可能看起来像这样:

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

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):              # __unicode__ on Python 2
        return self.tag

普通的ForeignKey只能“指向”一个其它的模型,这是说如果TaggedItem模型用一个 ForeignKey,它必须且只能选择为一个模型来保存tag。 Contenttypes应用提供一个特殊的字段(GenericForeignKey) 避免了这个问题并且允许和任何一个模型建立关联关系:

class GenericForeignKey

建立GenericForeignKey需要三个步骤:

  1. 给你的模型设置一个ForeignKey字段到ContentType 一般命名为“content_type”。
  2. 给你的模型设置一个字段,用来储存你想要关联的模型的主键值。 对于大多数模型,,这是一个PositiveIntegerField字段。 这个名字通常命名为“object_id”。
  3. 给你的模型设置一个GenericForeignKey字段, 把上面提到的那两个字段的名称传给它。 如果这两个字段名字分别为“content_type” 和 “object_id”, 你就可以省略他们 – GenericForeignKey 默认的会自动去查找这个两个命名的字段。
for_concrete_model

如果为False,那么字段将可以引用代理模型。 默认是True 它映射for_concrete_model参数到get_for_model()

主键类型的兼容性。

“object_id” 字段并不总是相同的,这是由于储存在相关模型中的主键类型的关系,但是他们的主键值必须被转变为相同类型,这是通过 “object_id” 字段 的 get_db_prep_value()方法。

例如, 如果你想要简历generic 关系到一个IntegerField或者CharField 为主键的模型, 你可以使用 CharField 给 “object_id”字段,因为数字是可以被 get_db_prep_value()转化为字母的。

为了更大的灵活性你也可以用 TextField ,一个没有限制最大长度的字段,,然而这可能给你的数据库带来巨大的影响。

这里没有一个一刀切的解决办法来应对哪个字段类型最好的问题。 你应该估摸一下。哪些models 你想要关联,并根据此决定一个最佳方案。(废话。)

序列化ContentType 的对象引用。

如果你想要序列化一个建立了 generic关系的model数据(for example, when generating fixtures) ,你应该用一个自然键来唯一的标识相关的 ContentType 对象。 有关详细信息,请参阅natural keysdumpdata --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>

由于 GenericForeignKey 完成的方式问题,,你没有办法用这个字段直接执行数据库API,filters的操作。比如 (filter() and exclude(), for example) 。 因为一个 GenericForeignKey不是一个普通的字段对象t, 这些例子是不会工作的:

# 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.db import models
from django.contrib.contenttypes.fields import GenericRelation

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

TaggedItems的每个实例都会有一个tags 属性,可以用来获取相关的 Bookmark:

>>> 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>]>

定义一个GenericRelation 伴有related_query_name可以允许从相关联的对象中查询。

tags = GenericRelation(TaggedItem, related_query_name='bookmarks')

这允许你从Bookmark执行过滤筛选, 排序, 和其他的查询操作TaggedItem

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

当然,如果你没有添加一个反向的关系,你可以手动做相同类型的查找:

>>> b = Bookmark.objects.get(url='https://www.djangoproject.com/')
>>> bookmark_type = ContentType.objects.get_for_model(b)
>>> TaggedItem.objects.filter(content_type__pk=bookmark_type.id, object_id=b.id)
<QuerySet [<TaggedItem: django>, <TaggedItem: python>]>

正如GenericForeignKey接受内容类型和对象ID字段的名称作为参数,GenericRelation也是如此。如果具有通用外键的模型对这些字段使用非默认名称,则必须在设置GenericRelation时传递字段的名称。 例如,假如 object_primary_key model关联到上述所用的字段用 content_type_fkTaggedItem 两个名称来创建一个 generic foreign 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’s database aggregation 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)

使用modelformset_factory()返回GenericInlineFormSet

如果它们分别与默认值,content_typefk_field不同,则必须提供ct_fieldobject_id 其他参数与modelformset_factory()inlineformset_factory()中记录的参数类似。

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

管理后台中的通用关系

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

这些类和函数确保了generic relations在forms 和 admin的使用。 有关详细信息,请参阅model formsetadmin文档。

class GenericInlineModelAdmin

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

ct_field

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

ct_fk_field

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

class GenericTabularInline
class GenericStackedInline

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