contenttypes フレームワーク

Django には contenttypes アプリケーションが含まれており、 Django を使ったプロジェクトにインストールされているすべてのモデルを追跡し、モデルを扱うための高レベルで汎用的なインターフェースを提供します。

概要

contenttypes アプリケーションの中心にあるのは、 django.contrib.contenttypes.models.ContentType にある ContentType モデルです。 ContentType のインスタンスは、プロジェクトにインストールされたモデルに関する情報を表し、格納します。新しい ContentType のインスタンスは、新しいモデルがインストールされるたびに自動的に作成されます。

ContentType のインスタンスは ContentType が表すモデルクラスを返すメソッドと、それらのモデルからオブジェクトを問い合わせるメソッドを持っています。また、 ContentType を操作するためのメソッドや、特定のモデルの ContentType のインスタンスを取得するためのメソッドを追加する カスタムマネージャ もあります。

あなたのモデルと ContentType の間のリレーションシップを利用することで、あなたのモデルのインスタンスとインストールされている任意のモデルのインスタンスとの間で「汎用的(ジェネリック)」なリレーションシップを有効にすることもできます。

contenttypes フレームワークをインストールする

contenttypes フレームワークは、django-admin startproject によって作成されるデフォルトの INSTALLED_APPS リストに含まれていますが、もし削除したり、手動で INSTALLED_APPS リストを設定した場合は、 'django.contrib.contenttypes'INSTALLED_APPS 設定に追加することで有効にできます。

通常は、contenttypes フレームワークをインストールしておくのがよいでしょう。Django の他のバンドルアプリケーションのいくつかはそれを必要とします:

  • admin アプリケーションは、管理インターフェイスを通じて追加・変更された各オブジェクトの履歴を記録するためにこれを使用します。
  • Django の 認証フレームワーク は、ユーザーの権限を特定のモデルに紐づけるのに使用されます。

ContentType モデル

class ContentType

ContentType の各インスタンスは、2 つのフィールドを持ち、それらを合わせると、インストールされたモデルを一意に特定します:

app_label

モデルが属するアプリケーションの名前です。これはモデルの app_label 属性から取得され、アプリケーションの Python インポートパスの 最後 の部分のみが含まれます。例えば、 django.contrib.contenttypesapp_labelcontenttypes になります。

model

モデルクラスの名前。

さらに、次のプロパティが利用可能です:

name

コンテンツタイプの人間が読める名前です。これはモデルの 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 が表すモデルの有効な lookup 引数 のセットを受け取り、そのモデル上で get() ルックアップ を実行し、対応するオブジェクトを返します。

ContentType.model_class()

この ContentType インスタンスが表すモデルクラスを返します。

たとえば、User モデルの ContentType を調べることができます。

>>> 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() を併用することで、2つの非常に重要なユースケースが可能になります:

  1. これらのメソッドを使うことで、インストールされた任意のモデルに対してクエリを実行する高レベルの汎用コードを書くことができます。特定のモデルクラスをインポートして使うのではなく、実行時に app_labelmodelContentType ルックアップに渡すことで、モデルクラスを操作したり、そこからオブジェクトを取得したりすることができます。
  2. ContentType は、別のモデルと関連付けることができ、これによってそのインスタンスを特定のモデルクラスに紐付けることができます。これらのメソッドを使用することで、これらのモデルクラスにアクセスできます。

Django のバンドルされているアプリケーションのいくつかは、後者のテクニックを利用しています。例えば、Django の認証フレームワークの パーミッションシステム は、 Permission モデルを ContentType の外部キーとして使っています。これにより、 Permission は「ブログのエントリを追加できる」とか「ニュース記事を削除できる」といった概念を表現できます。

ContentTypeManager

class ContentTypeManager

ContentType にはカスタムマネージャー ContentTypeManager もあり、以下のメソッドが追加されています:

clear_cache()

ContentType が使用する内部キャッシュをクリアし、 ContentType インスタンスを作成したモデルの追跡を行います。おそらく、このメソッドを自分で呼び出す必要はほとんどありません。必要なタイミングでDjangoが自動的に呼び出します。

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_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.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 の設定には3つの部分があります:

  1. モデルに ContentType への ForeignKey を持たせます。通常、このフィールドの名前は "content_type" となります。
  2. モデルに、リレーション先モデルの主キー値を格納できるフィールドを与えます。ほとんどのモデルの場合、これは PositiveIntegerField を意味します。このフィールドの通常の名前は "object_id" です。
  3. モデルに GenericForeignKey を与え、上記の2つのフィールド名を渡します。これらのフィールドの名前が "content_type" と "object_id" である場合、省略できます。これらは GenericForeignKey が探すデフォルトのフィールド名です。

ForeignKey とは異なり、データベースインデックスは GenericForeignKey には自動的に作成 されません ので、 Meta.indexes を使って独自の複数カラムインデックスを追加することをお勧めします。この動作は将来 変更されるかも知れません

for_concrete_model

False の場合、フィールドはプロキシモデルを参照できます。デフォルトは True です。これは get_for_model() への引数 for_concrete_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 を参照してください。

これにより、通常の 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 フィールドは元の値のままとなり、 GenericForeignKeyNone を返します:

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

同様に、 GenericForeignKeyModelForm 内には表示されません。

逆ジェネリックリレーション (Reverse generic relation)

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

related_query_name を設定して GenericRelation を定義すると、リレーション先オブジェクトからクエリを行うことができます。

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_fkobject_primary_key というフィールドを使用してジェネリック外部キーを作成した場合、 GenericRelation を以下のように定義する必要があります:

tags = GenericRelation(
    TaggedItem,
    content_type_field="content_type_fk",
    object_id_field="object_primary_key",
)

また、 GenericRelation を持つオブジェクトを削除した場合、 GenericForeignKey を持つオブジェクトも削除されることに注意してください。上の例では、 Bookmark オブジェクトが削除された場合、それを指す TaggedItem オブジェクトも同時に削除されることを意味します。

GenericForeignKey は、ForeignKey とは異なり、 on_delete 引数を受け入れず、この動作をカスタマイズすることはできません。必要なら、 GenericRelation を使用せずに、カスケード削除を回避でき、 代替の動作は pre_delete シグナルを介して提供できます。

ジェネリックリレーション (generic relation) と集計 (aggregation)

Django のデータベース集計 APIGenericRelation で動作します。例えば、すべてのブックマークのタグの数を調べることができます:

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

modelformset_factory() を使用して GenericInlineFormSet を返します。

デフォルトの content_typeobject_id と異なる場合は、それぞれ ct_fieldfk_field を指定する必要があります。その他のパラメータは modelformset_factory()inlineformset_factory() で説明されているものと同様です。

for_concrete_model 引数は GenericForeignKeyfor_concrete_model 引数に対応します。

admin におけるジェネリックリレーション

django.contrib.contenttypes.admin モジュールは、 GenericTabularInlineGenericStackedInline (GenericInlineModelAdmin のサブクラス) を提供します。

これらのクラスと関数は、フォームや admin アプリケーションでジェネリックリレーションを利用できるようにします。詳細に、モデルフォームセット および admin アプリケーション のドキュメントを参照してください。

class GenericInlineModelAdmin

GenericInlineModelAdmin クラスは InlineModelAdmin クラスの全てのプロパティを継承します。しかし、ジェネリックリレーションを扱うための独自のプロパティをいくつか追加しています:

ct_field

モデルの外部キーフィールド ContentType の名前。デフォルトは content_type です。

ct_fk_field

リレーション先オブジェクトの ID を表す整数フィールドの名前。デフォルトは object_id です。

class GenericTabularInline
class GenericStackedInline

GenericInlineModelAdmin のサブクラスで、それぞれスタックレイアウトと表形式のレイアウトを持っています。

GenericPrefetch()

New in Django 5.0.
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