読者です 読者をやめる 読者になる 読者になる

GenericRelationを使ってタグ付けをする

Django

 一部で話題の、GenericRelationと多重継承を組み合わせて使うテクニックについて紹介します。欠点がたくさんあるのですが、最初に思いつくであろうバージョンを示します。
 時間を見つけて、もっとよい方法、もっと詳しい解説を書きたいです。


 下のコードは、

http://www.djangoproject.com/documentation/models/generic_relations/

から例を借用しています。

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

class Taggable(object):
    def tag_with(self, csv):
        tag_names = [x.strip() for x in csv.split(',')]
        self.tags.clear()
        for tag in tag_names:
            if tag:
                self.tags.create(tag=tag)

class TaggedItem(models.Model):
    """A tag on an item."""
    tag = models.SlugField()
    content_type = models.ForeignKey(ContentType)
    object_id = models.PositiveIntegerField()
    
    content_object = models.GenericForeignKey()
    
    class Meta:
        ordering = ["tag"]
    
    def __str__(self):
        return self.tag

class Animal(models.Model, Taggable):
    common_name = models.CharField(maxlength=150)
    latin_name = models.CharField(maxlength=150)
    tags = models.GenericRelation(TaggedItem)

    def __str__(self):
        return self.common_name


 最初に登場するTaggableクラスが最大のポイントなのですが、まずは、GenericRelationの基本事項として、次の二点を挙げておきます。

  • GenericForeignKeyを定義したクラス(ここではTaggedItem)の主キーは、Integerでなくてはならい。
  • GenericForeignKeyによって参照されるクラスに、GenericRelationを定義する。この制約は、これはDB層ではなく、アプリケーション層で実現される。


 ここまでが、GenericForeignKey&GenericRelationのごくごく基本的な概念です。
 既存のコードに4-5行足すだけで、試してみることができると思うので、是非やってみて下さい。
 で、やってみると分かるのですが、Blogエントリ、Wikiページ、バグ・チケット、ToDo、メッセージ、写真、etc...とタグ付け可能オブジェクトが増えてくると、オブジェクト登録&タグ登録というコードを書くのが非常に面倒になってきます。


 そこで、最初のTaggableクラスが登場するわけす。このクラスを継承し、かつ、tagsという属性名でGenericRelationを定義したモデルクラスのオブジェクトは、次のような感じで、簡単にタグ付けを行えます。


 あらかじめ登録してある、猫オブジェクトにタグ付けを行います。

>>> from leuchtturm.sandbox.models import *
>>> cat = Animal.objects.get(pk=1)
>>> cat
<Animal: Cat>
>>> cat.tag_with("spam, egg, ham")
>>> cat.tags.all()
[<TaggedItem: egg>, <TaggedItem: ham>, <TaggedItem: spam>]


 新しく犬オブジェクトを追加して、タグ付けするなら、

>>> dog = Animal.objects.create(common_name="Dog")
>>> dog.tag_with("ham")
>>> dog.tags.all()
[<Tagged: ham>]


 タグから、オブジェクトを検索する場合は、こんな感じ。

>>> [x.content_object for x in TaggedItem.objects.filter(tag="ham")]
[<Animal: Cat>, <Animal: Dog>]


 なんだか変な例で申し訳ないのですが、基本的には、この多重継承路線でどんなオブジェクトに対しても、タグ付けができます。他の方法で、ぱっと思いつくのは、メタクラスを使う方法。まだ試していませんが。


 上のコードの問題点をざっと述べておくと、

  • TaggedItemのSlugFieldの冗長性を排除できていない。
  • Taggableクラスのインスタンスは、それ自身では存在できないことをもっと明確にすべき。
  • タグの更新、登録のロジック(tag_with)が貧弱。


 また、GenericRelationは開発途上のためもあって、真剣に使うならば索引の張り方や、クエリの発行の仕方などを考慮する必要があります。何も考えないで使うと、気が狂うほどのクエリが実行されるので注意が必要です。