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))
少々面倒だがこうするしかない。
Re:Re:mercurialでチケット駆動開発
元記事 mercurialでチケット駆動開発 - logiqboard
id:monjudoh から懸念点を指摘されたのでもーちょっと考えてみる。
Re:mercurialでチケット駆動開発 - monjudoh’s diary
confirmで動作してもdefaultでの動作が保障されない
例えばデカい機能の新規リリースが控えている場合、「チケットxxxが、confirmにマージされたその変更に依存して正常動作している」という状況が起こりえる。
ただ元記事でも言われているように、この運用はサービスリリース後のバグフィックス・機能修正フェーズに入った後に固めた方法なので、
問題がおこらなかったという感じでしょうか。
認識を改める
confirmブランチは、実情として
"修正、機能追加の確認をする場所"
ではなく
"リリース済み成果に対する修正内容を確認する場所"
という運用がされていたというわけです。
なので機能追加に関してはこの運用だけでは対応し切れない。
じゃあどうする
"リリース前の機能Xに対する修正、機能実装の確認をする場所"
というのがあればいいんじゃなかろうか。
リポジトリ状態(想像
- 中央(hg push releaseでpushできるようにしておく)
- default ... どんなときでもリリース可能状態に保つ
- 開発環境(push先のdefaultに設定
- default
- confirm ... リリース済み成果に対する修正を集める(確認中成果も混ざる
- dev_xxx ... 機能xxxの開発ブランチ。機能xxxの開発成果を集める*1
- dev_yyy
- 中央@個人(=個人ローカル)(hg push myでry
- default
- confirm
- dev_xxx ... 機能開発を担当している開発ブランチ
- ticketnnn ... 担当チケットのトピックブランチ*2
開発の進め方(想像
1. 大型機能xxxの開発に着手
2. dev_xxxブランチを切り、開発環境にpush
hg pull -r default release hg up default hg branch dev_xxx hg ci -m "start dev_xxx" hg push -r dev_xxx
3. 以下の手順をループ
- 機能xxxに対するチケットaaaのトピックブランチを作成
hg pull -r dev_xxx hg up dev_xxx hg branch ticketaaa hg ci -m "start tikcetaaa"
- チケット処理が完了したらdev_xxxにマージし、開発環境にpush
hg up dev_xxx hg merge ticketaaa hg ci -m "merge ticketaaa->dev_xxx" hg push -r dev_xxx
- defaultに変更があった場合はdev_xxxをrebase
hg pull -r default release hg up dev_xxx hg merge default hg ci -m "rebase"
4. 機能xxxの動作確認は、開発環境のブランチをdev_xxxに切り替えて行う
hg up -C dev_xxx
5. 確認が取れたらdefaultにmerge
hg pull -r default release hg up default hg merge dev_xxx hg ci -m "release dev_xxx" hg push -r default release
前回との違い
チケットを直接defaultにmergeしてリリース状態にする代わりに、機能ブランチにチケットを集約するというワンクッションを置くようにした。
実際のところ、1チケットを処理する為の作業量もリリース状態にする際の作業量も前回とほとんど変わりないはず。
confirmブランチを使った確認と組み合わせて使えば、大体の状況には対応できそう。
どこからが「機能」か
- チケット間に依存がある
- 1チケット処理しただけではリリース出来ない=confirmブランチにマージしても確認できない。
- 担当者が複数居る
- チケットの変更分を機能ブランチで共有しないといけない。
上記を満たすなら機能ブランチを切る必要がある。
依存するチケットが数枚程度+担当者が自分一人の場合は機能ブランチを切らなくてもなんとかなる。
ただしdefaultへマージするときは、どのチケットをマージしなくてはいけないかよくよく確認しないといけない。