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へマージするときは、どのチケットをマージしなくてはいけないかよくよく確認しないといけない。

まとめ


少々複雑さが増してしまった感はあるが

  • cloneするリポジトリの数は増やさなくて済んだ
  • defaultは常にリリース可能状態に保たれている
  • 関係ない人に開発ブランチを伝播させないで済む
  • チケット処理の手順は増えていない


ので、悪くなさそう。

あとはチケットの粒度にも気を払わなきゃいけないとか、この運用をしてくれない人がリポジトリを触る状況になるとめんどくさいとか、結構考えることは多い。

うまく回せればかなり柔軟に開発が進められると思うので、mercurialを使ってる方々は一度お試しください。

*1:ブランチ名はdev_xxxとかプレフィクス+名前とするのがいいです。branchesしたときに非常にわかりやすいので。

*2:機能ブランチの注釈に同じ。更にnnnの部分はチケット番号のほうがいいです。自分で名前を付けると絶対忘れるので、チケットとの対応を取るときにめんどくさいです。