contenttypes フレームワーク

revision-up-to:7127 (0.97pre SVN)

Django には、 contenttypes アプリケーションが付属しています。 このアプリケーションを使うと、 Django で作成したプロジェクト中に存在する全 てのモデルを追跡できます。また、 contenttypes では、モデルを扱うための 高水準かつ汎用的なインタフェースを提供しています。

概要

contenttypes アプリケーションの心臓部は、 ContentType モデル であり、 django.contrib.contenttypes.models.ContentType で定義されてい ます。 ContentType のインスタンスは、プロジェクト上にインストールされて いるモデルの情報を表現したり保存したりしています。新たにモデルをインストー ルすると、 ContentType が自動的に生成されます。

ContentType のインスタンスは、インスタンスの表現するモデルクラス を返したり、そのモデルクラスに対してオブジェクトをクエリするためのメソッド を提供しています。また、 ContentType モデルは ContentType を扱った り、特定のモデルの ContentType を取り出すための カスタムマネジャ を 持っています。

あるモデルから ContentType にリレーションを張れば、そのモデルからインス トール済みの任意のモデルのインスタンスとの間に、「一般化リレーション (generic relation)」を張れます。

contenttypes フレームワークのインストール

contenttypes フレームワークは、 django-admin.py startproject の生成するデフォルトの settings.pyINSTALLED_APPS リストに 入っています。リストから除外してしまっている場合や、 INSTALLED_APPS を 自分で設定した場合には、 INSTALLED_APPS'django.contrib.contenttypes' を追加してフレームワークをインストールし てください。

通常は、 contenttypes フレームワークをインストールしておいた方がよいで しょう。 Django にバンドルされている以下のアプリケーションには、 contenttypes フレームワークが必要だからです:

  • admin アプリケーションは、管理インタフェース上で追加変更したオブ ジェクトの履歴を管理するために contenttypes を使います。
  • Django の 認証フレームワーク はユーザパーミッションをモデルに結び つけるさために contenttypes を使います。
  • Django のコメントシステム (django.contrib.comments) は、インストー ルされている任意のモデルに対してコメントを付けられる機能を contenttypes で実現しています。

ContentType モデル

ContentType のインスタンスには 3 つのフィールドがあり、 3 つを組み合わ せると、インストールされているモデルを一意に表現できます:

app_label
モデルの入っているアプリケーションの名前です。この名前はモデルの app_label 属性から取り出され、通常は Python の import パス の 末尾 の部分が入っています。例えば、アプリケーションが django.contrib.contenttypes なら、 app_labelcontenttypes です。
model
モデルクラスの名前です。
name
人間可読なモデル名です。この値は、モデルの verbose_name 属性 から取り出されます。

例を挙げて、 contenttypes の仕組みを見てみましょう。 contenttypes アプリケーションをインストールしておき、 INSTALLED_APPS 設定に sites アプリケーション を追加してから、 manage.py syncdb を実行して みてください。 django.contrib.sites.models.Site モデルがデータベースに インストールされるはずです。それに伴って、以下の値がセットされた ContentType の新たなインスタンスが生成されているはずです:

  • app_label'sites' (django.contrib.sites の末尾) に設定 されています。
  • model'site' に設定されています。
  • name'site' に設定されています。

ContentType インスタンスのメソッド

ContentType インスタンスには、インスタンスの表現しているモデルクラスを 取得したり、モデルクラスのインスタンスを取り出したりするためのメソッドがあ ります:

get_object_for_this_type(**kwargs)
ContentType インスタンスの表現しているモデルでサポートされてい る 照合メソッドの引数 を取り、モデルインスタンスの get() 照合 を行って対応するオブジェクトを返します。
model_class()
ContentType インスタンスの表現しているモデルクラスを返します。

例を挙げましょう。まず ContentType から、 User モデルの ContentType インスタンスを照合します:

>>> from django.contrib.contenttypes.models import ContentType
>>> user_type = ContentType.objects.get(app_label="auth", model="user")
>>> user_type
<ContentType: user>

取り出した UserContentType から、 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_typemodel_class を組み合わせると、非常に重 要なユースケースを実現できます:

  1. 二つのメソッドを使えば、特定のモデルクラスを import して使うのではな く、インストールされている全てのモデルに対して操作を行えるような一般 化された高水準のコードを書けます。実行時に app_labelmodel を指定して ContentType を照合し、得られたモデルクラス からオブジェクトを取り出せるのです。
  2. 別のモデルから ContentType にリレーションを張って、モデルのイン スタンスと、 ContentType の表すモデルクラスを結び付け、モデルク ラスのメソッドにアクセスできます。

Django にバンドルされているアプリケーションには、後者のテクニックを使ってい るものがいくつかあります。例えば、 Django の認証フレームワークに入っている パーミッションのシステム は、 ContentType への外部キーを張っています。 そうすることで、 Permission は「ブログにエントリを追加」や「ニュースス トーリーを削除」といった権限を表現できます。

ContentTypeManager カスタムマネジャ

ContentType には ContentTypeManager というカスタムマネジャがありま す。このカスタムマネジャには、以下のメソッドがあります:

clear_cache()
ロード済みの ContentType インスタンスを保持しておくための内部 キャッシュをクリアします。このメソッドを自分で呼ぶことはほとんどな いでしょう。 Django は必要に応じて自動的にメソッドを呼び出します。
get_for_model(model)
モデルクラスやモデルのインスタンスを引数にとり、そのモデルを表す ContentType インスタンスを返します。

ContentType を扱いたいけれども、わざわざモデルのメタデータを取り出し て ContentType を手動で照合するのが面倒な場合には get_for_model メソッドが特に便利です:

>>> from django.contrib.auth.models import User
>>> user_type = ContentType.objects.get_for_model(User)
>>> user_type
<ContentType: user>

一般化リレーション

上で述べた Permission モデルの例のように、自作のモデルから ContentType に外部キーを張れば、モデルを別のモデルクラスに結び付けられ ます。しかし、もう一歩踏み込めば、 ContentType を使って真の 一般化リレーション (または多態性: polymorphic リレーション) を実現できます。

簡単な例として、以下のようなタグシステムを挙げましょう:

from django.db import models
from django.contrib.contenttypes.models import ContentType
from django.contrib.contenttypes import generic

class TaggedItem(models.Model):
    tag = models.SlugField()
    content_type = models.ForeignKey(ContentType)
    object_id = models.PositiveIntegerField()
    content_object = generic.GenericForeignKey('content_type', 'object_id')

    def __unicode__(self):
        return self.tag

通常の ForeignKey は、他のモデルを「指し示す」だけに過ぎません。従って、 TaggedItem モデルで ForeignKey を使う場合、ただ一つのモデルに対して しかタグを保存できません。この問題を解決し、任意のモデルにリレーションを張 れるようにするために、 contenttypes アプリケーションでは、特殊なフィー ルドタイプ、 django.contrib.contenttypes.generic.GenericForeignKey を提 供しています。 GenericForeignKey のセットアップは、以下の 3 つのパート に分かれています:

  1. ContentType への ForeignKey を定義します。
  2. リレーション先のモデルインスタンスの主キー値を保存するためのモデル フィールドを定義します (ほとんどのモデルでは、 IntegerField また は PositiveIntegerField です)。
  3. GenericForeignKey を定義します。 GenericForeignKey の引数に、 上で定義したフィールドの名前を指定します。フィールド名が GenericForeignKey の探すデフォルトの名前である content_typeobject_id の場合には、引数を省略してかまいません。

これで、通常の ForeignKey に似た API を実現できます。 TaggedItem はリレーションを張っている対象のモデルインスタンスを返す content_object フィールドを持つようになります。 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>

逆方向の一般化リレーション

リレーション対象にどのモデルがよく使われるのかが分かっていれば、 「逆方向の」一般化リレーションを張って、 API を追加できます。例を挙げましょ う:

class Bookmark(models.Model):
    url = models.URLField()
    tags = generic.GenericRelation(TaggedItem)

これで、 Bookmark インスタンスは tags 属性をそなえます。この属性を 使うと、インスタンスにリレーションを張っている TaggedItems を取り出せま す:

>>> b = Bookmark(url='http://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()
[<TaggedItem: django>, <TaggedItem: python>]

逆方向のリレーションを張らなければ、手動で照合する必要があります:

>>> b = Bookmark.objects.get(url='http://www.djangoproject.com/)
>>> bookmark_type = ContentType.objects.get_for_model(b)
>>> TaggedItem.objects.filter(content_type__pk=bookmark_type.id,
...                           object_id=b.id)
[<TaggedItem: django>, <TaggedItem: python>]

GenericRelation を持つオブジェクトを削除すると、このオブジェクトに GenericForeignKey でリレーションを張っているオブジェクトも全て削除され るので注意してください。つまり、上の例では、 Bookmark オブジェクト b を削除すると、 b にリレーションを張っている t1t2 といっ た TaggedItem も同時に削除されるのです。