Make組ブログ

Python、Webアプリや製品・サービス開発についてhirokikyが書きます。

DjangoのField.__init__ でクエリーしてはいけない

動的にフォームの choices の値を作りたい場合など、フォームの内容のためにクエリーすることはよくあると思います。 でも、フィールドの __init__ でクエリーしてしまうコードを書くとインポート時に実行されてしまうので注意が必要です。

ダメな例

class FoobarChoiceField(forms.ChoiceField):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        self.choices = [(foobar.name, foobar.title)
                        for foobar in Foobar.objects.all()]


class HogeForm(forms.Form):
    foobar = FoobarChoiceForm()

理由

フィールドの __init__ はインポート時に実行されてしまうので、インポート時に(上記の場合Foobarへの)クエリーが実行されてしまいます。 インポート時にHogeFormが作成されて、FoobarChoiceField.__init__ も実行されます。 choicesがリストの場合、そのままクエリーも実行されてしまいます。

マイグレーション時(DBが作られる前)などに実行されてしまうとFoobarに対応するテーブルがまだ作られていないのでエラーになります。 また、サーバー起動時にクエリーされる場合、DBに変更があってもchoicesの値が変わらなくなってしまいます。

直す例

Django1.8からchoices指定にcallableを渡せるので、callableにしておきましょう。

class FoobarChoiceField(forms.ChoiceField):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        self.choices = lambda: [(foobar.name, foobar.title)
                                for foobar in Foobar.objects.all()]

もちろん、フィールドを作らない場合はFormでやってもOK。 まぁFormの __init__ は(普通に書く分には)インポート時に呼び出されないので、1.8以前のように __init__ で choices を作るように書いてもOKです。

class HogeForm(forms.Form):
    foobar = ChoiceForm(choices=lambda: [(foobar.name, foobar.title) for foobar in Foobar.objects.all()])