デフォルトマネージャという地雷

djangoのモデルデータはマネージャというオブジェクトを介して取得します。

独自定義のマネージャを使うことでデフォルトとは違う挙動をさせることができて便利ですが、地雷もありますよという話。

http://docs.djangoproject.com/en/1.2/topics/db/managers/#custom-managers

マネージャのカスタマイズ

class MyManager(models.Manager):
    def get_query_set(self):
        return super(MyManager, self).get_query_set().filter(is_deleted=False)
        
    def with_user(self, user):
        return self.filter(user=user)
        
class MyModel(models.Model):
    is_deleted = models.BooleanField(u"削除フラグ", default=False)
    user = models.ForeignKey(User, verbose_name=u"ユーザー")

    published = MyManager()

こんなふうに定義すると

>>> MyModel.published.all()
[is_deleted=Falseのモデル全部]

>>> MyModel.published.with_user(user)
[is_deleted=False, user=userのモデル]

>>> MyModel.objects.all()
[全てのMyModel]

と、publishedマネージャを使うだけで自動的に削除フラグの立っているものを除外するフィルタを掛けられて、大変便利なわけです。

地雷

モデルにはデフォルトマネージャというものがあり、明示的に指定しない場合はそのクラス定義の中で一番最初に見つかったマネージャオブジェクトをデフォルトマネージャとして使うようになっています。

なので、MyModelの場合はpublishedに使われているMyManagerがデフォルトマネージャとして使われることになります。

爆発

  1. ☠adminの挙動が狂う☠

adminではdefault_managerを使ってオブジェクトの一覧を取得します。

なので、is_deletedフラグを落とすとMyModelの管理画面上から消滅します。

つまり、表示状態に戻せません。

  1. ☠逆引き時のマネージャ☠

上記の定義だと

>>> user.mymodel_set.all()

としてリレーションを逆に辿ることができるわけですが、このmymodel_setはRelatedManagerというオブジェクトで、デフォルトマネージャをベースに動的に作られます。

つまり、暗黙のうちにis_deleted=Falseというフィルタがここにも掛かっているということになります。

取れて欲しいオブジェクトが取れなかったり、取れないはずのオブジェクトが取れたりして大変不愉快な挙動を示します。

回避

デフォルトマネージャは極力素のmodels.Managerオブジェクトにしておくとよいです。

class MyModel(models.Model):
    is_deleted = models.BooleanField(u"削除フラグ", default=False)
    user = models.ForeignKey(User, verbose_name=u"ユーザー")

    objects = models.Manager()
    published = MyManager()

こうすれば1番目がManagerになるので地雷は回避できます。もしくは、default_manager = models.Manager()というのでもいいです。

むしろ

managerを複数生やす=自分で地雷を埋めているようなものなので、よっぽど必然性が無い限りはobjects一つにしておくのが無難です。

publishedで実現したかった機能(is_deleted=Trueのオブジェクトを簡単に外す)を実現したければ、例えばget_query_setでやってることを普通のメソッドに移すなどの方法があります。

class MyManager(models.Manager):
    def be(self):
        return self.filter(is_deleted=False)
        
    def with_user(self, user):
        return self.be().filter(user=user)

>>> MyModel.objects.be().filter(...)
[is_deleted=False+その他の条件で絞りこまれたオブジェクト]

おわり

3/50

早くもペースがやばいです。