DjangoのモデルのManagerクラスのメソッドをメソッドチェインさせる

※追記:django-model-utils というパッケージの PassThroughManager が同じようなことしてくれるみたいです
Model Managers — django-model-utils 2.3a1 documentation


例えば下記のような、書籍名・本棚登録済みかのフラグ・出版日を持ったモデルクラスがあるとする

from django.db import models


class Book(models.Model):
    name = models.CharField(max_length=255)
    registered_flg = models.BooleanField(default=False)
    published_date = models.DateTimeField()

ここで、「本棚登録済みの本のみを抽出」とか「出版済みの本のみを抽出」とかしたい場合、マネージャクラスを使う。

class BookManager(models.Manager):
    def registered(self):
        return self.filter(registered_flg=True)

    def published(self):
        return self.filter(published_date__lte=datetime.datetime.now())

class Book(models.Model):
    ...(中略)
    objects = BookManager()

さてこれで目的は果たせたわけだけど、ここでさらに「すでに出版されていて尚且つ本棚登録済みのもの」のみを抽出したい場合どうすればいいだろう。

Managerのメソッドの戻り値に対して別のManagerメソッドをチェイン出来ない

>>> Book.objects.published().registered()

これはAttributeErrorになる。publishedメソッドが返すのはQuerySet。当然registeredなんてメソッドは持っていない。もちろんQuerySetのfilterメソッドをチェインすることは出来るけど...

>>> Book.objects.published().filter(registered_flg=True)

だからと言ってBookManagerに

def registered_and_published(self):
    return self.filter(published_date__lte=datetime.datetime.now(),
                       registered_flg=True)

なんてメソッドを追加するのもちょっと…メソッドの数が増えてくると面倒な事になる。出来ればメソッドチェインさせたい。

QuerySetのカスタマイズ

QuerySetをカスタマイズすることで解決できる

class BookQuerySet(models.query.QuerySet):
    def registered(self):
        return self.filter(registered_flg=True)

    def published(self):
        return self.filter(published_date__lte=datetime.datetime.now())


class BookManager(models.Manager):
    def get_query_set(self):
        return BookQuerySet(self.model)

    def registered(self):
        return self.get_query_set().registered()

    def published(self):
        return self.get_query_set().published()


class Book(models.Model):
    name = models.CharField(max_length=255)
    registered_flg = models.BooleanField(default=False)
    published_date = models.DateTimeField()

    objects = BookManager()

コレでほぼOKなんだけど、このままだとBookQuerySetにメソッドを追加するたびにBookManagerにもメソッド追加しないといけない。それは冗長なので、BookMangerに__getattr__を持たせるとコードがすっきりする。これでBookQuerySetにいくらメソッドを増やそうが、__getattr__が上手いことやってくれる。

bpythonとかで補完が効かなくなるけど、まあそこはしょうがないか。

最終的なコードはこんな感じ
import datetime
from django.db import models


class BookQuerySet(models.query.QuerySet):
    def registered(self):
        return self.filter(registered_flg=True)

    def published(self):
        return self.filter(published_date__lte=datetime.datetime.now())


class BookManager(models.Manager):
    def get_query_set(self):
        return BookQuerySet(self.model)

    def __getattr__(self, name):
        return getattr(self.get_query_set(), name)


class Book(models.Model):
    name = models.CharField(max_length=255)
    registered_flg = models.BooleanField(default=False)
    published_date = models.DateTimeField()

    objects = BookManager()