GenericForeignKey

今回の主題


GFKきもいです

GenericForeignKeyとは


"一般化リレーション"を実現する、django.contrib.contenttypesの機能のひとつです。

要はどんなモデルにでもリレーション張れるモデルが作れる。

つかう


settings.pyのinstalled_appsへのdjango.contrib.contenttypesの組み込みとsyncdbは忘れずに。

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

  class Tag(models.Model):                                                             
    name = models.CharField(max_length=255)                                          
                                                                                      
    content_type = models.ForeignKey(ContentType)                                    
    object_id = models.PositiveIntegerField()                                        
    target = generic.GenericForeignKey()                                             
                                                                                      
  class Hoge(models.Model):                                                            
    title = models.CharField(max_length=255)                                         
                                                                                      
    tags = generic.GenericRelation(Tag)                                              
                                                                                      
  class Fuga(models.Model):                                                            
    name = models.CharField(max_length=255)                                          
                                                                                      
    tags = generic.GenericRelation(Tag)                                              
                                                                                     
  class Foo(models.Model):                                                             
    title = models.CharField(max_length=255)                                         
    count = models.IntegerField()                                                    
                                                                                      
    tags = generic.GenericRelation(Tag)                                              

こんなモデルを作って

In [14]: Hoge(name=u"hoge1").save()
In [15]: Hoge(name=u"hoge2").save()
In [16]: Hoge(name=u"hoge3").save()

In [17]: Fuga(title=u"fuga1").save()
In [18]: Fuga(title=u"fuga2").save()
In [19]: Fuga(title=u"fuga3").save()

In [20]: Foo(title=u"foo1", count=1).save()
In [21]: Foo(title=u"foo2", count=2).save()
In [22]: Foo(title=u"foo3", count=3).save()

In [27]: hoges = Hoge.objects.order_by("title")
In [31]: fugas = Fuga.objects.order_by("name")
In [33]: foos = Foo.objects.order_by("count")

In [36]: Tag(name=u"tag_hoge1 A", target=hoges[0]).save()
In [37]: Tag(name=u"tag_hoge1 B", target=hoges[0]).save()
In [38]: Tag(name=u"tag_hoge2 C", target=hoges[1]).save()
In [40]: Tag(name=u"tag_fuga1 D", target=fugas[0]).save()
In [41]: Tag(name=u"tag_fuga2 E", target=fugas[1]).save()
In [42]: Tag(name=u"tag_fuga3 F", target=fugas[2]).save()
In [43]: Tag(name=u"tag_fuga3 G", target=fugas[2]).save()
In [44]: Tag(name=u"tag_foo2 A", target=foos[1]).save()
In [45]: Tag(name=u"tag_foo2 B", target=foos[1]).save()
In [46]: Tag(name=u"tag_foo2 C", target=foos[1]).save()
In [47]: Tag(name=u"tag_foo3 D", target=foos[2]).save()

こんなデータを入れる。

Tagから引く

In [48]: Tag.objects.all()
Out[48]: [<Tag: Tag object>, <Tag: Tag object>, <Tag: Tag object>, <Tag: Tag object>, <Tag: Tag object>, <Tag:
 <Tag: Tag object>, <Tag: Tag object>, <Tag: Tag object>, <Tag: Tag object>, <Tag: Tag object>]

In [51]: tags[0]
Out[51]: <Tag: Tag object>

In [52]: tags[0].target
Out[52]: <Hoge: Hoge object>

In [53]: tags[4].target
Out[53]: <Fuga: Fuga object>

Tagで引いても勝手に紐付けた型のモデルインスタンスがtargetに入っている。これは便利すぎる。

要はGenericForeignKeyを張ると、GFKのフィールドには紐付いているテーブルから引っ張った適切な型のインスタンスを入れてくれるというワケ。

以前の記事にあるとおり、ContentTypeの値を持ってさえいれば自動的に紐付いているテーブルとインスタンスの型が判別できるから、そこを自動化しただけなんだろうけど・・・書ける気はしない。

GenericRelationからTagを引く

In [60]: Fuga.objects.filter(tags__name__contains=u"tag_fuga3")
Out[60]: [<Fuga: Fuga object>, <Fuga: Fuga object>]

これも平気。というかこれは単なるmodels.ForeignKeyと大差ない。

複雑なクエリ


今度はちょっと複雑なクエリを生成してみる。

In [56]: Tag.objects.filter(target__title__contains=u"1")
FieldError: Cannot resolve keyword 'target' into field. Choices are: content_type, foo, fuga, hoge, id, name, object_id

targetはDBフィールドではないので、これはだめ。

ただよく見ると

'target'などというフィールドは無い。こっから選べ: content_type, foo, fuga, hoge ...

foo, fuga, hogeで引けるようになっているらしい。これはGenericRelationの効果のようだ。なので

In [74]: hoge_q = Q(hoge__title__contains=u"1")
In [75]: fuga_q = Q(fuga__name__contains=u"1")
In [76]: foo_q = Q(foo__count=1)
In [77]: tags = Tag.objects.filter(hoge_q|fuga_q|foo_q)

In [78]: for tag in tags:
   ....:     print tag.name, type(tag.target)
   ....:
   ....:
tag_hoge1 A <class 'tag.models.Hoge'>
tag_hoge1 B <class 'tag.models.Hoge'>
tag_fuga1 D <class 'tag.models.Fuga'>

こんなひどいクエリが作れる。きもすぎる。

注意点


#11535 (GenericRelation query with OR creates incorrect SQL) – Django

これは正しい使い方なようだが、GFK自体にバグがあるらしく妙なSQLを吐く様子。

('SELECT "tag_tag"."id", "tag_tag"."name", "tag_tag"."content_type_id", "tag_tag"."object_id" FROM "tag_tag" INNER JOIN "ta
g_tag" T2 ON ("tag_tag"."id" = T2."id") LEFT OUTER JOIN "tag_hoge" ON (T2."object_id" = "tag_hoge"."id") LEFT OUTER JOIN "t
ag_fuga" ON (T2."object_id" = "tag_fuga"."id") LEFT OUTER JOIN "tag_foo" ON (T2."object_id" = "tag_foo"."id") WHERE ("tag_h
oge"."title" LIKE %s ESCAPE \'\\\'  OR "tag_fuga"."name" LIKE %s ESCAPE \'\\\'  OR "tag_foo"."count" = %s )',
 (u'%1%', u'%1%', 1))

たとえばさっきのTag.objectに対するクエリは上のようになるが、content_type_idに関するWHERE句がないので、上でTagを引いたときのように複数のモデルに対するQをorで繋いだりすると、正しく取れない可能性がある。

回避

In [81]: hoge_q = Q(hoge__title__contains=u"1", conetnt_type=ContentType.objects.get_for_model(Hoge))
In [82]: fuga_q = Q(fuga__name__contains=u"1", conetnt_type=ContentType.objects.get_for_model(Fuga))
In [83]: foo_q = Q(foo__count=1, conetnt_type=ContentType.objects.get_for_model(Foo))


少々面倒だがこうするしかない。