Djangoで、カスタムマネージャーを使う

Python Django

概要

Djangoでは、モデル名.objectsのようにすると様々なことができます。このobjectsはマネージャーと呼ばれるものです。

このマネージャーは自分でカスタマイズすることもできます。

初期クエリを変更したくなる例

例えばカテゴリと記事のモデルがあり、カテゴリの一覧を表示する際に紐づいた記事数も一緒に表示したいとします。

これはDjangoで、集計処理でも紹介したannotate()で実装することができます。

次のようなモデルの例です。

from django.db import models


class Category(models.Model):
    name = models.CharField('カテゴリ名', max_length=255)

    def __str__(self):
        return self.name


class Post(models.Model):
    title = models.CharField('タイトル', max_length=255)
    category = models.ForeignKey(Category, verbose_name='カテゴリ', on_delete=models.PROTECT)

    def __str__(self):
        return self.title

例えば、カテゴリの一覧表示をするビューは次のようになるでしょう。

from django.db.models import Count
from django.views import generic
from .models import *


class CaegorytList(generic.ListView):
    model = Category

    def get_queryset(self):
        queryset = super().get_queryset()
        # カテゴリを、紐づいた記事数と一緒に取得し、その記事数順に並び替え
        return queryset.annotate(post_count=Count('post')).order_by('-post_count')

そして、category_list.htmlです。

{% extends 'app/base.html' %}

{% block content %}
    <!-- Bulmaで書いています-->
    <div class="section">
        <div class="category">
            {% for category in category_list %}
                <p>{{ category }}({{ category.post_count }})</p>
            {% endfor %}
        </div>
    </div>
{% endblock %}

これは問題なく動きます。
ちゃんと記事数も一緒に表示される

今のところ、ビューのget_queryset()を上書きするだけで済みます。

しかしこの表示は便利なので、admin管理画面でもやりたくなるかもしれません。また、記事検索フォームでのカテゴリ欄でも同様に表示するとわかりやすいでしょう。

こうなってくると、カテゴリを表示しそうな箇所全てを↑のようなコードにする必要があります。

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

そこで、マネージャーのカスタマイズです。models.pyを編集します。

# 増えた
class CategoryManager(models.Manager):
    def get_queryset(self):
        return super().get_queryset().annotate(
            post_count=models.Count('post')
        ).order_by('-post_count')


class Category(models.Model):
    name = models.CharField('カテゴリ名', max_length=255)
    objects = CategoryManager()  # 足した

    def __str__(self):
        return self.name

Categoryのobjects属性を、CategoryManagerというものに変更しました。`

モデルの標準ではobjects = models.Manager()のようになっていて、Category.objects.all()のような処理をすると、マネージャークラスのget_queryset()が呼ばれます。今回のコードはそれを上書きしている形ですね。

初期クエリの段階で記事数も一緒に返すようになったので、__str__()での表示名も一緒に変更しておくと捗るでしょう。

class Category(models.Model):
    name = models.CharField('カテゴリ名', max_length=255)
    objects = CategoryManager()

    def __str__(self):
        if hasattr(self, 'post_count'):
            return f'{self.name}({self.post_count})'
        else:
            return self.name

すると、管理画面でも記事数がパッと確認できますね。
管理画面でも記事数が表示された

よく使うfilter処理をマネージャーに定義する

例えば記事に未来の日付を設定することができて、現在時間までの記事の一覧と、未来も含めたすべての記事一覧が欲しいかもしれません。filterでの絞り込みも勿論可能ですが、こういった場合、次のようなマネージャーを定義しておくと捗ります。

from django.db import models
from django.utils import timezone


class PostManager(models.Manager):

    def published(self):
        return self.get_queryset().filter(created_at__lte=timezone.now())


class Post(models.Model):
    title = models.CharField('タイトル', max_length=255)
    text = models.TextField('本文')
    created_at = models.DateTimeField('作成日', default=timezone.now)

    objects = PostManager()

    def __str__(self):
        return self.title

この例はget_queryset()を上書きしていません。その代わり、published()という独自のメソッドを定義しています。処理内容を見ればわかるように、これは現在時間までの記事だけ取得します。これにより、Post.objects.all()で全ての記事が取得でき、Post.objects.published()で公開記事だけを取得できるのです。ビュー等にfilter()で絞り込みをする必要はなくなり、直感的になりました。

もちろん、独自のメソッドは好きに追加できます。例えば未来の記事だけを取得したいならば、次のようにできるでしょう。

class PostManager(models.Manager):

    def published(self):
        return self.get_queryset().filter(created_at__lte=timezone.now())

    def private(self):
        return self.get_queryset().filter(created_at__gt=timezone.now())

これは当然、Post.objects.private()で呼び出せます。

更に、マネージャーは複数利用できます。

class PostManager(models.Manager):

    def published(self):
        return self.get_queryset().filter(created_at__lte=timezone.now())

    def private(self):
        return self.get_queryset().filter(created_at__gt=timezone.now())


# 増えた
class PostNaritoManager(models.Manager):

    def get_queryset(self):
        return super().get_queryset().filter(user__username='narito')


class Post(models.Model):
    title = models.CharField('タイトル', max_length=255)
    text = models.TextField('本文')
    created_at = models.DateTimeField('作成日', default=timezone.now)
    user = models.ForeignKey(settings.AUTH_USER_MODEL, verbose_name='投稿者')  # 増えた

    objects = PostManager()
    narito = PostNaritoManager()  # 増えた

    def __str__(self):
        return self.title

まず、記事にユーザーを紐付けてみました。そしてobjectsのほか、naritoというクラス属性にもマネージャーを指定しています。新しく作ったPostNaritoManagerは、get_querysetメソッドを上書きし、投稿者がnaritoの記事だけに絞り込みます。つまり、Post.narito.all()とすると、投稿者がnaritoの記事が予め取得される訳です。PostManagerのように、publishedのようなメソッドを定義することも、勿論可能です。

好きにマネージャークラスを追加できるし、マネージャークラスにも、好きにメソッドを追加できます。get_querysetメソッドを上書きすれば、初期クエリも変更できました。

ちなみにですが、ForeignKeyで逆側から一覧が欲しいときがありますね。例えばユーザ一覧を表示しながら、ユーザーが投稿した記事の一覧も表示したいケースです。テンプレートファイルで、次のように書くことになるでしょう。

{% for usr in user_list %}
    {{ usr.username }}
    {% for post in usr.post_set.all %}
        この投稿者の記事一覧を取り出す

この場合も、マネージャーのメソッドが呼び出せます。つまり、各ユーザーが投稿した、公開記事一覧とかも取れるわけですね。

{% for usr in user_list %}
    {{ usr.username }}
    {% for post in usr.post_set.published %}
        この投稿者の記事一覧を取り出す

マネージャーとクエリセット

一点注意なのは、今までのコードはあくまでマネージャークラスにメソッドを追加しています。クエリセットは、これらのメソッドを持っていません

例えば、マネージャークラスに次のようなメソッドが増えました。

class PostManager(models.Manager):

    def published(self):
        return self.get_queryset().filter(created_at__lte=timezone.now())

    def private(self):
        return self.get_queryset().filter(created_at__gt=timezone.now())

    def popular(self):
        return self.get_queryset().filter(人気な記事)

popularメソッドです。これは、人気な記事を返します。公開されてて、人気な記事が欲しいならば、次のようなコードを書けそうですが...

Post.objects.popular().published()

残念ながら、このコードはエラーです。Post.objects.popular()で返されるのはクエリセットであって、マネージャークラスではありません。クエリセットには、publishedメソッドはありません。それがあるのは、あくまでマネージャーです。マネージャーと、クエリセットは別物なのです

publishedメソッドを、次のようにすることはできますが...

return self.get_queryset().filter(created_at__lte=timezone.now()).filter(人気な記事)

人気ではない公開記事が欲しいときは、どうすればよいでしょう。似たようなメソッドを作るのでしょうか。それをするのも一つの解決策ですが、他の方法もあります。マネージャーと同様にクエリセットクラスも定義できるのです。

from django.db import models
from django.utils import timezone


# ここ
class PostQuerySet(models.QuerySet):

    def published(self):
        return self.filter(created_at__lte=timezone.now())

    def private(self):
        return self.filter(created_at__gt=timezone.now())

    def popular(self):
        return self.filter(人気記事)


class Post(models.Model):
    title = models.CharField('タイトル', max_length=255)
    text = models.TextField('本文')
    created_at = models.DateTimeField('作成日', default=timezone.now)

    objects = PostQuerySet.as_manager()  # ここ

    def __str__(self):
        return self.title

PostQuerySetというクエリセットを作成しました。そして、モデルにはobjects = PostQuerySet.as_manager()としています。これは何かというと、クエリセットクラスを素にマネージャークラスを作るという意味です。

これにより、Post.objects.published()は勿論動作しますし、Post.objects.published().popular()も動作します。マネージャーにもクエリセットにも同様のメソッドがあるので、呼び出せるという訳ですね。

Relation Posts

Djangoで、集計処理

Djangoで、データの平均値を求めたり、何個のデータが紐づくか、といった集計処理のサンプルを紹介しています。

Python Django

Comment

記事にコメントする

まだコメントはありません。