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.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
(**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つの非常に重要なユースケースが可能になります:
- これらのメソッドを使うことで、インストールされた任意のモデルに対してクエリを実行する高レベルの汎用コードを書くことができます。特定のモデルクラスをインポートして使うのではなく、実行時に
app_label
とmodel
をContentType
ルックアップに渡すことで、モデルクラスを操作したり、そこからオブジェクトを取得したりすることができます。 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つの部分があります:- モデルに
ContentType
へのForeignKey
を持たせます。通常、このフィールドの名前は "content_type" となります。 - モデルに、リレーション先モデルの主キー値を格納できるフィールドを与えます。ほとんどのモデルの場合、これは
PositiveIntegerField
を意味します。このフィールドの通常の名前は "object_id" です。 - モデルに
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
フィールドは元の値のままとなり、 GenericForeignKey
は None
を返します:
>>> 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)
同様に、 GenericForeignKey
は ModelForm
内には表示されません。
逆ジェネリックリレーション (Reverse generic relation)¶
-
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 []>
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_fk
と object_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 のデータベース集計 API は GenericRelation
で動作します。例えば、すべてのブックマークのタグの数を調べることができます:
>>> Bookmark.objects.aggregate(Count("tags"))
{'tags__count': 3}
フォームにおけるジェネリックリレーション¶
django.contrib.contenttypes.forms
モジュールは、下記を提供します:
BaseGenericInlineFormSet
GenericForeignKey
を使用するためのフォームセットファクトリ、generic_inlineformset_factory()
。
-
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_type
とobject_id
と異なる場合は、それぞれct_field
とfk_field
を指定する必要があります。その他のパラメータはmodelformset_factory()
とinlineformset_factory()
で説明されているものと同様です。for_concrete_model
引数はGenericForeignKey
のfor_concrete_model
引数に対応します。
admin におけるジェネリックリレーション¶
django.contrib.contenttypes.admin
モジュールは、 GenericTabularInline
と GenericStackedInline
(GenericInlineModelAdmin
のサブクラス) を提供します。
これらのクラスと関数は、フォームや admin アプリケーションでジェネリックリレーションを利用できるようにします。詳細に、モデルフォームセット および admin アプリケーション のドキュメントを参照してください。
-
class
GenericInlineModelAdmin
¶ GenericInlineModelAdmin
クラスはInlineModelAdmin
クラスの全てのプロパティを継承します。しかし、ジェネリックリレーションを扱うための独自のプロパティをいくつか追加しています:-
ct_field
¶ モデルの外部キーフィールド
ContentType
の名前。デフォルトはcontent_type
です。
-
ct_fk_field
¶ リレーション先オブジェクトの ID を表す整数フィールドの名前。デフォルトは
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>]>