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))
少々面倒だがこうするしかない。