• 3.1
  • 3.2
  • 5.0
  • Версия документации: 6.1

Фреймворк контентных типов

В 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 framework uses 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 со следующими значениями:

  • Атрибут app_label со значением 'sites' (последняя часть django.contrib.sites).

  • Атрибут model со значением 'site'.

Методы экземпляра 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(), дает нам два крайне важных и полезных варианта их использования:

  1. Воспользовавшись этими методами, вы можете писать высокоуровневый , обобщенный(generic) код, и выполнять запрос к любой установленной в приложении модели. Вместо того, чтобы импортировать конкретные модели «по одиночке», вы можете передать нужные параметры app_label и model в класс ContentType во время выполнения, и затем работать с полученной моделью класса или получить конкретные объекты этой модели.

  2. Вы можете связывать другую модель с 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:

  1. Создайте в вашей модели поле типа ForeignKey, указав в качестве внешней модели ContentType. Обычно такому полю дают имя «content_type».

  2. Дайте вашей модели поле, которое может хранить значения первичного ключа из моделей, к которым вы будете иметь отношение. Для большинства моделей это означает PositiveBigIntegerField. Обычное имя этого поля — «object_id».

  3. Создайте в вашей модели поле типа 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

По умолчанию обратная связь не создается. Чтобы создать такую связь, укажите параметр 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 предлагает нам следующее:

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>]>
Back to Top