Фреймворк contenttypes
Django включает приложение contenttypes, которое отслеживает все модели, установленные в вашем проекте на Django, и предоставляет высокоуровневый универсальный интерфейс для работы с моделями.
Обзор
В основе приложения contenttypes лежит модель ContentType, расположенная в django.contrib.contenttypes.models.ContentType. Экземпляры ContentType представляют и хранят информацию о моделях, установленных в вашем проекте, а новые экземпляры ContentType создаются автоматически при установке новых моделей.
У экземпляров ContentType есть методы для получения представляемых ими классов моделей и выполнения запросов к объектам этих моделей. У ContentType также есть пользовательский менеджер, который добавляет методы для работы с ContentType и получения экземпляров ContentType для определённой модели.
Связи между вашими моделями и ContentType также можно использовать для создания «универсальных» связей между экземпляром одной из ваших моделей и экземплярами любой установленной модели.
Установка фреймворка contenttypes
Фреймворк contenttypes включён в список INSTALLED_APPS по умолчанию, создаваемый командой django-admin startproject. Если вы удалили его или вручную настроили список INSTALLED_APPS, включить его можно, добавив 'django.contrib.contenttypes' в параметр INSTALLED_APPS.
Как правило, фреймворк contenttypes стоит устанавливать: он требуется нескольким другим приложениям Django, входящим в комплект поставки:
- Приложение администрирования использует его для ведения истории каждого объекта, добавленного или изменённого через интерфейс администрирования.
authentication frameworkDjango использует его для привязки разрешений пользователей к определённым моделям.
Модель ContentType
-
class ContentType[исходный код] -
У каждого экземпляра
ContentTypeесть два поля, которые вместе однозначно описывают установленную модель:-
app_label -
Имя приложения, к которому относится модель. Оно берётся из атрибута
app_labelмодели и включает только последнюю часть пути импорта Python приложения; например,django.contrib.contenttypesстановится значениемapp_labelcontenttypes.
-
model -
Имя класса модели.
Кроме того, доступно следующее свойство:
-
name[исходный код] -
Понятное человеку имя типа содержимого. Оно берётся из атрибута
verbose_nameмодели.
-
Рассмотрим пример, чтобы увидеть, как это работает. Если приложение contenttypes уже установлено, а затем вы добавите the sites application в параметр INSTALLED_APPS и запустите manage.py migrate для его установки, модель django.contrib.sites.models.Site будет установлена в вашей базе данных. Вместе с ней будет создан новый экземпляр ContentType со следующими значениями:
Методы экземпляров ContentType
У каждого экземпляра ContentType есть методы, позволяющие получить из экземпляра ContentType соответствующую ему модель или получить объекты этой модели:
-
ContentType.get_object_for_this_type(using=None, **kwargs)[исходный код] -
Принимает набор допустимых аргументов поиска для модели, представляемой объектом
ContentType, и выполняет для этой моделиa get() lookup, возвращая соответствующий объект. Аргумент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() позволяют реализовать два чрезвычайно важных сценария:
- С помощью этих методов можно писать универсальный высокоуровневый код, выполняющий запросы к любой установленной модели: вместо импорта и использования одного конкретного класса модели можно передать
app_labelиmodelв запросContentTypeво время выполнения, а затем работать с классом модели или получать объекты из неё. - Можно связать другую модель с
ContentType, чтобы привязать её экземпляры к определённым классам моделей, а затем использовать эти методы для доступа к соответствующим классам моделей.
Несколько приложений Django, входящих в комплект поставки, используют второй способ. Например, the permissions system во фреймворке аутентификации Django использует модель Permission с внешним ключом на ContentType. Это позволяет Permission представлять такие понятия, как «может добавить запись в блог» или «может удалить новость».
ContentTypeManager
-
class ContentTypeManager[исходный код] -
У
ContentTypeтакже есть пользовательский менеджерContentTypeManager, который добавляет следующие методы:-
clear_cache()[исходный код] -
Очищает внутренний кэш, который
ContentTypeиспользует для отслеживания моделей, для которых были созданы экземплярыContentType. Скорее всего, вам никогда не понадобится вызывать этот метод самостоятельно: Django вызовет его автоматически при необходимости.
-
get_for_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по естественному ключу при десериализации.
-
Метод get_for_model() особенно полезен, когда вам нужно работать с объектом ContentType, но вы не хотите получать метаданные модели для выполнения поиска вручную:
>>> 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.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, поле сможет ссылаться на прокси-модели. По умолчанию используетсяTrue. Это соответствует аргументуfor_concrete_modelметодаget_for_model().
- Добавьте в модель
Совместимость типов первичных ключей
Поле «object_id» не обязательно должно иметь тот же тип, что и поля первичных ключей связанных моделей, но значения их первичных ключей должны приводиться к тому же типу, что и поле «object_id», с помощью метода get_db_prep_value().
Например, если вы хотите разрешить универсальные связи с моделями, у которых поля первичных ключей имеют тип IntegerField или CharField, для поля «object_id» в модели можно использовать CharField, поскольку метод get_db_prep_value() может приводить целые числа к строкам.
Для максимальной гибкости можно использовать TextField без заданной максимальной длины, однако это может существенно снизить производительность в зависимости от используемой СУБД.
Не существует универсального решения, которое определяло бы наилучший тип поля. Вам следует оценить модели, на которые предполагается ссылаться, и выбрать наиболее эффективное решение для своего случая.
Сериализация ссылок на объекты ContentType
При сериализации данных (например, при создании fixtures) из модели, в которой реализованы универсальные связи, для уникальной идентификации связанных объектов ContentType, вероятно, следует использовать естественный ключ. Подробнее см. в разделах естественные ключи и dumpdata --natural-foreign.
Это позволяет использовать API, похожий на 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)
Аналогично, 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")
Это позволяет фильтровать, сортировать и выполнять другие операции с запросами к 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>]>
Если не добавлять 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, принимающий в качестве аргументов имена полей типа содержимого и идентификатора объекта, 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.
Универсальные связи и агрегирование
API агрегирования базы данных Django работает с GenericRelation. Например, можно узнать, сколько тегов у всех закладок:
>>> Bookmark.objects.aggregate(Count("tags"))
{'tags__count': 3}
Универсальная связь в формах
Модуль django.contrib.contenttypes.forms предоставляет:
BaseGenericInlineFormSet- Фабрику наборов форм
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иfk_field; по умолчанию это соответственноcontent_typeиobject_id. Другие параметры аналогичны описанным в документации дляmodelformset_factory()иinlineformset_factory().Аргумент
for_concrete_modelсоответствует аргументуfor_concrete_modelуGenericForeignKey.
Универсальные связи в административном интерфейсе
Модуль django.contrib.contenttypes.admin предоставляет GenericTabularInline и GenericStackedInline (подклассы GenericInlineModelAdmin).
Эти классы и функции позволяют использовать универсальные связи в формах и административном интерфейсе. Подробнее см. документацию по наборам форм модели и административному интерфейсу.
-
class GenericInlineModelAdmin[исходный код] -
Класс
GenericInlineModelAdminнаследует все свойства классаInlineModelAdmin. Кроме того, он добавляет несколько собственных свойств для работы с универсальной связью:-
ct_field -
Имя поля внешнего ключа на
ContentTypeв модели. По умолчанию —content_type.
-
ct_fk_field -
Имя целочисленного поля, представляющего идентификатор связанного объекта. По умолчанию —
object_id.
-
-
class GenericTabularInline[исходный код]
-
class GenericStackedInline[исходный код] -
Подклассы
GenericInlineModelAdminс вертикальным и табличным расположением соответственно.
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>]>
© Django Software Foundation and individual contributors
Licensed under the BSD License.
https://docs.djangoproject.com/en/6.0/ref/contrib/contenttypes/