Фреймворк контентных типов¶
В Django входит приложение contenttypes, которое позволяет отслеживать все модели вашего Django проекта. Это приложение предоставляет высокоуровневый, обобщенный интерфейс для работы с вашими моделями.
Обзор¶
В основе приложения contenttypes лежит модель ContentType, которая находится в django.contrib.contenttypes.models.ContentType. Экземпляр ContentType представляет и хранит информацию о моделях, использующихся в вашем проекте, и новые экземпляры модели ContentType создаются автоматически при добавлении новых моделей в проект.
У экземпляров ContentType есть методы, позволяющие получить класс модели, который они представляют, или запросить объекты этой модели. У модели ContentType имеется также собственный менеджер, который предоставляет методы для работы с классом ContentType и для получения экземпляров ContentType для конкретной модели.
Взаимосвязь между ContentType и вашими моделями можно использовать для создания «обобщенных» («generic» ) отношений между экземпляром вашей модели и экземпляром любой другой модели в проекте.
Установка и подключение contenttypes¶
Фреймворк contenttypes включен по умолчанию и находится в списке INSTALLED_APPS файла настроек, созданного вызовом команды django-admin startproject. Если вам необходимо отключить фреймворк или добавить его вручную, просто удалите (или добавьте) в список INSTALLED_APPS приложение 'django.contrib.contenttypes'.
Рекомендуется всегда подключать фреймворк контентных типов в проект, поскольку его наличие требуется для работы ряда других встроенных приложений Django:
Встроенное приложение администрирования Django использует его для журналирования истории добавления или изменения объектов через админку.
Django’s
authentication frameworkuses it to tie user permissions to specific models.
Модель ContentType¶
- class ContentType¶
Каждый экземпляр
ContentTypeсодержит два поля, которые вместе уникальным образом описывают каждую модель в приложении:- app_label¶
Имя приложения в которое входит данная модель. Данные берутся из атрибута
app_labelмодели и включают в себя только последнюю часть пути, который используется для импорта модели. Н-р, в случае «django.contrib.contenttypes» используется значение атрибутаapp_labelдля «contenttypes».
- model¶
Имя модели класса.
Также доступны следующие свойства:
- name¶
«Читабельное» имя модели. Берется из атрибута
verbose_nameмодели.
Покажем на примере как это все работает. Если приложение contenttypes уже установлено, то добавьте приложение sites в INSTALLED_APPS файла настроек и выполните команду manage.py migrate для создания таблиц и завершения установки модели django.contrib.sites.models.Site. Параллельно с этим будет создан новый экземпляр ContentType со следующими значениями:
Методы экземпляра ContentType¶
Каждый экземпляр ContentType имеет методы, которые позволяют получить доступ к модели, представленной этим экземпляром ContentType , или получить объекты этой модели:
- ContentType.get_object_for_this_type(using=None, **kwargs)¶
Принимает набор допустимых аргументов поиска <field-lookups-intro>` для модели, которую представляет
ContentType, и выполняетa get() поискдля этой модели, возвращая соответствующий объект. Аргументusingможет использоваться для указания другой базы данных, отличной от базы данных по умолчанию.
- 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(), дает нам два крайне важных и полезных варианта их использования:
Воспользовавшись этими методами, вы можете писать высокоуровневый , обобщенный(generic) код, и выполнять запрос к любой установленной в приложении модели. Вместо того, чтобы импортировать конкретные модели «по одиночке», вы можете передать нужные параметры
app_labelиmodelв классContentTypeво время выполнения, и затем работать с полученной моделью класса или получить конкретные объекты этой модели.Вы можете связывать другую модель с
ContentTypeдля привязки экземпляров этой модели с любыми произвольными классами моделей проекта, и использовать указанные методы для доступа к этим моделям.
Некоторые из встроенных приложений Django используют последний подход. Н-р, в системе полномочий( permissions system), в фреймворке аутентификации Django, в модели Permission используется внешний ключ(foreign key) к ContentType; это позволяет создать обобщенную связь с различными моделями и реализовать концепцию ограничений, такую как «пользователь может добавить запись в блог» или «пользователь может удалить сообщение из новостей».
ContentTypeManager¶
- class ContentTypeManager¶
Класс
ContentTypeобладает собственным менеджером,ContentTypeManager, который включает в себя следующие методы:- clear_cache()¶
Очищает внутренний кеш, используемый
ContentTypeдля отслеживания моделей, для которых он создал экземплярыContentType. Вероятно, вам не потребуется самостоятельно вызывать этот метод в коде приложения; Django вызовет его автоматически, когда это необходимо.Вам может потребоваться очистить кеш во время тестирования, выполнить сброс между тестами или после подготовки состояния теста. Например:
class ContentTypesTests(TestCase): def setUp(self): ContentType.objects.clear_cache() self.addCleanup(ContentType.objects.clear_cache)
- get_for_id(id)¶
Получение экземпляра
ContentTypeпо идентификатору(ID). Поскольку данный метод использует тот же разделяемый кэш, что и метод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_model=Falseпозволяет получитьContentTypeдля прокси-модели.
- get_by_natural_key(app_label, model)¶
Возвращает экземпляр
ContentType, однозначно определенный переданной меткой приложения и именем модели. Главной задачей этого метода является предоставление возможности ссылаться на объектыContentTypeпосредством натуральных ключей в процессе десериализации.
Метод get_for_model() особенно полезен, когда вы знаете, что вам нужно работать с ContentType, но не хотите беспокоиться о получении метаданных модели для выполнения ручного поиска:
>>> from django.contrib.auth.models import User
>>> ContentType.objects.get_for_model(User)
<ContentType: user>
Обобщенные связи(generic relations)¶
Добавление внешнего ключа в одну из ваших меделей на 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.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 GenericForeignKey¶
Существуют три правила по созданию и настройке
GenericForeignKey:Создайте в вашей модели поле типа
ForeignKey, указав в качестве внешней моделиContentType. Обычно такому полю дают имя «content_type».Дайте вашей модели поле, которое может хранить значения первичного ключа из моделей, к которым вы будете иметь отношение. Для большинства моделей это означает
PositiveBigIntegerField. Обычное имя этого поля — «object_id».Создайте в вашей модели поле типа
GenericForeignKey, и передайте ему в качестве аргументов, имена полей созданных ранее. Если эти поля названы «content_type» и «object_id», вы можете не передавать их, – эти имена используются вGenericForeignKeyпо умолчанию.
В отличие от
ForeignKey, индекс базы данных не автоматически создается вGenericForeignKey, поэтому рекомендуется использоватьMeta.indexesдля добавления собственного индекса с несколькими столбцами. Такое поведение может измениться в будущем.- for_concrete_model¶
При
False, поле может ссылаться на прокси-модель. Отображает аргументfor_concrete_modelметодаget_for_model(). По умолчанию равноTrue.
Тип первичного ключа
Поле «object_id» не обязательно должно быть того же типа, что и у первичного ключа в привязанной модели, но должно соблюдаться условие, что значения первичного ключа могут быть приведены к тому же типу, что и у поля «object_id « методом get_db_prep_value().
Н-р, если вы хотите создать обобщенные отношения с моделями, использующими в качестве первичных ключей типы IntegerField или CharField, вы можете использовать тип CharField для вашего поля «object_id», поскольку целочисленные значения могут быть корректно приведены к строковым методом get_db_prep_value().
Для максимальной гибкости можно использовать тип TextField, который не накладывает ограничения на длину строки, но такое решение может негативно отразиться на производительности, в зависимости от используемой базы данных.
Не существует универсального решения для выбора наилучшего типа поля. Вы должны изучить модели с которыми собираетесь работать, и на основании этого принять решение о выборе типа для поля, руководствуясь принципами эффективности.
Сериализация связей с ContentType
При сериализации данных модели (например, при создании fixtures) , которая имеет обобщенные связи, вам вероятно необходимо будет воспользоваться натуральным ключом, чтобы корректно определить связи с объектами ContentType. Смотрите натуральные ключи и dumpdata --natural-foreign для дополнительной информации.
Это активирует API, аналогичный тому, который используется для обычного ForeignKey; каждый 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 вы не можете использовать такие поля напрямую с фильтрами (например, filter() и exclude()) через API базы данных. Поскольку GenericForeignKey не является обычным объектом поля, эти примеры не будут работать:
# This will fail
>>> TaggedItem.objects.filter(content_object=guido)
# This will also fail
>>> TaggedItem.objects.get(content_object=guido)
Аналогично, GenericForeignKeys не отображаются в ModelForms.
Обратная обобщенная связь(reverse generic relations)¶
- 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")
Это позволяет фильтровать, упорядочивать и выполнять другие операции запроса к Bookmark из TaggedItem:
>>> # Get all tags belonging to bookmarks containing `django` in the url
>>> TaggedItem.objects.filter(bookmark__url__contains="django")
<QuerySet [<TaggedItem: django>, <TaggedItem: python>]>
Если вы не добавите связанное_имя_запроса, вы можете выполнять те же типы поиска вручную:
>>> 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, GenericRelation принимает аргументами имена полей content-type и object-ID . Если модель, имеющая обобщенный внешний ключ не использует имена по-умолчанию для этих полей, а любые другие, – вы должны передать эти имена полей в 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.
Обобщенные связи и агрегация¶
API агрегации базы данных Django работает с GenericRelation. Например, вы можете узнать, сколько тегов имеют все закладки:
>>> Bookmark.objects.aggregate(Count("tags"))
{'tags__count': 3}
Обобщенные связи в формах¶
Модуль django.contrib.contenttypes.forms предлагает нам следующее:
Фабрика создания набора форм,
generic_inlineformset_factory(), для использования сGenericForeignKey.
- 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)¶
Возвращает
GenericInlineFormSet, используяmodelformset_factory().Вы должны предоставить имена для
ct_fieldиobject_idесли они отличаются от значений по умолчанию, -content_typeиobject_id. Прочие параметры аналогичны тем, что описаны вmodelformset_factory()иinlineformset_factory().Аргумент
for_concrete_modelсоответствует параметруfor_concrete_modelдляGenericForeignKey.
Обобщенные связи в админке¶
Модуль django.contrib.contenttypes.admin предоставляет GenericTabularInline и GenericStackedInline (дочерние классы GenericInlineModelAdmin)
Эти классы и функции позволяют использовать обобщенные отношения объектов при создании форм и в админке Django. За дополнительной информацией обратитесь к модель набора форм и admin .
- class GenericInlineModelAdmin¶
Класс
GenericInlineModelAdminнаследует все свойства классаInlineModelAdmin. Но также имеет ряд собственных атрибутов для работы с обобщенными связями:- ct_field¶
Имя поля внешнего ключа
ContentTypeмодели. По умолчаниюcontent_type.
- ct_fk_field¶
Имя целочисленного поля, которое хранит идентификатор конкретного объекта связанной модели. По умолчанию
object_id.
- class GenericTabularInline¶
- class GenericStackedInline¶
Подклассы
GenericInlineModelAdminпозволяющие настраивать отображение данных в сложенном(stacked) или табличном(tabular) виде, соответственно.
GenericPrefetch()¶
- class 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.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>]>